diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml
index 4d5ed0f25f..5da1ec3a24 100644
--- a/.devcontainer/compose.yaml
+++ b/.devcontainer/compose.yaml
@@ -21,12 +21,13 @@ services:
       ES_HOST: es
       ES_PORT: '9200'
       LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000
+      LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000}
     # Overrides default command so things don't shut down after the process ends.
     command: sleep infinity
     ports:
-      - '127.0.0.1:3000:3000'
-      - '127.0.0.1:3035:3035'
-      - '127.0.0.1:4000:4000'
+      - '3000:3000'
+      - '3035:3035'
+      - '4000:4000'
     networks:
       - external_network
       - internal_network
diff --git a/.dockerignore b/.dockerignore
index 41da718049..9d990ab9ce 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -20,3 +20,9 @@ postgres14
 redis
 elasticsearch
 chart
+.yarn/
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
diff --git a/.env.production.sample b/.env.production.sample
index 1faaf5b57c..a311ad5f8d 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -79,6 +79,9 @@ AWS_ACCESS_KEY_ID=
 AWS_SECRET_ACCESS_KEY=
 S3_ALIAS_HOST=files.example.com
 
+# Optional list of hosts that are allowed to serve media for your instance
+# EXTRA_MEDIA_HOSTS=https://data.example1.com,https://data.example2.com
+
 # IP and session retention
 # -----------------------
 # Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml
@@ -86,3 +89,27 @@ S3_ALIAS_HOST=files.example.com
 # -----------------------
 IP_RETENTION_PERIOD=31556952
 SESSION_RETENTION_PERIOD=31556952
+
+# Fetch All Replies Behavior
+# --------------------------
+# When a user expands a post (DetailedStatus view), fetch all of its replies
+# (default: false)
+FETCH_REPLIES_ENABLED=false
+
+# Period to wait between fetching replies (in minutes)
+FETCH_REPLIES_COOLDOWN_MINUTES=15
+
+# Period to wait after a post is first created before fetching its replies (in minutes)
+FETCH_REPLIES_INITIAL_WAIT_MINUTES=5
+
+# Max number of replies to fetch - total, recursively through a whole reply tree
+FETCH_REPLIES_MAX_GLOBAL=1000
+
+# Max number of replies to fetch - for a single post
+FETCH_REPLIES_MAX_SINGLE=500
+
+# Max number of replies Collection pages to fetch - total
+FETCH_REPLIES_MAX_PAGES=500
+
+# Maximum allowed character count
+MAX_CHARS=5555
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index d4930e1f52..0000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,13 +0,0 @@
-/build/**
-/coverage/**
-/db/**
-/lib/**
-/log/**
-/node_modules/**
-/nonobox/**
-/public/**
-!/public/embed.js
-/spec/**
-/tmp/**
-/vendor/**
-!.eslintrc.js
diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 480b274fad..0000000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,367 +0,0 @@
-// @ts-check
-const { defineConfig } = require('eslint-define-config');
-
-module.exports = defineConfig({
-  root: true,
-
-  extends: [
-    'eslint:recommended',
-    'plugin:react/recommended',
-    'plugin:react-hooks/recommended',
-    'plugin:jsx-a11y/recommended',
-    'plugin:import/recommended',
-    'plugin:promise/recommended',
-    'plugin:jsdoc/recommended',
-  ],
-
-  env: {
-    browser: true,
-    node: true,
-    es6: true,
-  },
-
-  parser: '@typescript-eslint/parser',
-
-  plugins: [
-    'react',
-    'jsx-a11y',
-    'import',
-    'promise',
-    '@typescript-eslint',
-    'formatjs',
-  ],
-
-  parserOptions: {
-    sourceType: 'module',
-    ecmaFeatures: {
-      jsx: true,
-    },
-    ecmaVersion: 2021,
-    requireConfigFile: false,
-    babelOptions: {
-      configFile: false,
-      presets: ['@babel/react', '@babel/env'],
-    },
-  },
-
-  settings: {
-    react: {
-      version: 'detect',
-    },
-    'import/ignore': [
-      'node_modules',
-      '\\.(css|scss|json)$',
-    ],
-    'import/resolver': {
-      typescript: {},
-    },
-  },
-
-  rules: {
-    'consistent-return': 'error',
-    'dot-notation': 'error',
-    eqeqeq: ['error', 'always', { 'null': 'ignore' }],
-    'indent': ['error', 2],
-    'jsx-quotes': ['error', 'prefer-single'],
-    'semi': ['error', 'always'],
-    'no-catch-shadow': 'error',
-    'no-console': [
-      'warn',
-      {
-        allow: [
-          'error',
-          'warn',
-        ],
-      },
-    ],
-    'no-empty': ['error', { "allowEmptyCatch": true }],
-    'no-restricted-properties': [
-      'error',
-      { property: 'substring', message: 'Use .slice instead of .substring.' },
-      { property: 'substr', message: 'Use .slice instead of .substr.' },
-    ],
-    'no-restricted-syntax': [
-      'error',
-      {
-        // eslint-disable-next-line no-restricted-syntax
-        selector: 'Literal[value=/•/], JSXText[value=/•/]',
-        // eslint-disable-next-line no-restricted-syntax
-        message: "Use '·' (middle dot) instead of '•' (bullet)",
-      },
-    ],
-    'no-unused-expressions': 'error',
-    'no-unused-vars': 'off',
-    '@typescript-eslint/no-unused-vars': [
-      'error',
-      {
-        vars: 'all',
-        args: 'after-used',
-        destructuredArrayIgnorePattern: '^_',
-        ignoreRestSiblings: true,
-      },
-    ],
-    'valid-typeof': 'error',
-
-    'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }],
-    'react/jsx-boolean-value': 'error',
-    'react/display-name': 'off',
-    'react/jsx-fragments': ['error', 'syntax'],
-    'react/jsx-equals-spacing': 'error',
-    'react/jsx-no-bind': 'error',
-    'react/jsx-no-useless-fragment': 'error',
-    'react/jsx-no-target-blank': ['error', { allowReferrer: true }],
-    'react/jsx-tag-spacing': 'error',
-    'react/jsx-uses-react': 'off', // not needed with new JSX transform
-    'react/jsx-wrap-multilines': 'error',
-    'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
-    'react/self-closing-comp': 'error',
-
-    // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46
-    'jsx-a11y/click-events-have-key-events': 'off',
-    'jsx-a11y/label-has-associated-control': 'off',
-    'jsx-a11y/media-has-caption': 'off',
-    'jsx-a11y/no-autofocus': 'off',
-    // recommended rule is:
-    // 'jsx-a11y/no-interactive-element-to-noninteractive-role': [
-    //   'error',
-    //   {
-    //     tr: ['none', 'presentation'],
-    //     canvas: ['img'],
-    //   },
-    // ],
-    'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off',
-    // recommended rule is:
-    // 'jsx-a11y/no-noninteractive-tabindex': [
-    //   'error',
-    //   {
-    //     tags: [],
-    //     roles: ['tabpanel'],
-    //     allowExpressionValues: true,
-    //   },
-    // ],
-    'jsx-a11y/no-noninteractive-tabindex': 'off',
-    // recommended is full 'error'
-    'jsx-a11y/no-static-element-interactions': [
-      'warn',
-      {
-        handlers: [
-          'onClick',
-        ],
-      },
-    ],
-
-    // See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js
-    'import/extensions': [
-      'error',
-      'always',
-      {
-        js: 'never',
-        jsx: 'never',
-        mjs: 'never',
-        ts: 'never',
-        tsx: 'never',
-      },
-    ],
-    'import/first': 'error',
-    'import/newline-after-import': 'error',
-    'import/no-anonymous-default-export': 'error',
-    'import/no-extraneous-dependencies': [
-      'error',
-      {
-        devDependencies: [
-          '.eslintrc.js',
-          'config/webpack/**',
-          'app/javascript/mastodon/performance.js',
-          'app/javascript/mastodon/test_setup.js',
-          'app/javascript/**/__tests__/**',
-        ],
-      },
-    ],
-    'import/no-amd': 'error',
-    'import/no-commonjs': 'error',
-    'import/no-import-module-exports': 'error',
-    'import/no-relative-packages': 'error',
-    'import/no-self-import': 'error',
-    'import/no-useless-path-segments': 'error',
-    'import/no-webpack-loader-syntax': 'error',
-
-    'import/order': [
-      'error',
-      {
-        alphabetize: { order: 'asc' },
-        'newlines-between': 'always',
-        groups: [
-          'builtin',
-          'external',
-          'internal',
-          'parent',
-          ['index', 'sibling'],
-          'object',
-        ],
-        pathGroups: [
-          // React core packages
-          {
-            pattern: '{react,react-dom,react-dom/client,prop-types}',
-            group: 'builtin',
-            position: 'after',
-          },
-          // I18n
-          {
-            pattern: '{react-intl,intl-messageformat}',
-            group: 'builtin',
-            position: 'after',
-          },
-          // Common React utilities
-          {
-            pattern: '{classnames,react-helmet,react-router,react-router-dom}',
-            group: 'external',
-            position: 'before',
-          },
-          // Immutable / Redux / data store
-          {
-            pattern: '{immutable,@reduxjs/toolkit,react-redux,react-immutable-proptypes,react-immutable-pure-component}',
-            group: 'external',
-            position: 'before',
-          },
-          // Internal packages
-          {
-            pattern: '{mastodon/**}',
-            group: 'internal',
-            position: 'after',
-          },
-        ],
-        pathGroupsExcludedImportTypes: [],
-      },
-    ],
-
-    'promise/always-return': 'off',
-    'promise/catch-or-return': [
-      'error',
-      {
-        allowFinally: true,
-      },
-    ],
-    'promise/no-callback-in-promise': 'off',
-    'promise/no-nesting': 'off',
-    'promise/no-promise-in-callback': 'off',
-
-    'formatjs/blocklist-elements': 'error',
-    'formatjs/enforce-default-message': ['error', 'literal'],
-    'formatjs/enforce-description': 'off', // description values not currently used
-    'formatjs/enforce-id': 'off', // Explicit IDs are used in the project
-    'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx
-    'formatjs/enforce-plural-rules': 'error',
-    'formatjs/no-camel-case': 'off', // disabledAccount is only non-conforming
-    'formatjs/no-complex-selectors': 'error',
-    'formatjs/no-emoji': 'error',
-    'formatjs/no-id': 'off', // IDs are used for translation keys
-    'formatjs/no-invalid-icu': 'error',
-    'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings
-    'formatjs/no-multiple-whitespaces': 'error',
-    'formatjs/no-offset': 'error',
-    'formatjs/no-useless-message': 'error',
-    'formatjs/prefer-formatted-message': 'error',
-    'formatjs/prefer-pound-in-plural': 'error',
-
-    'jsdoc/check-types': 'off',
-    'jsdoc/no-undefined-types': 'off',
-    'jsdoc/require-jsdoc': 'off',
-    'jsdoc/require-param-description': 'off',
-    'jsdoc/require-property-description': 'off',
-    'jsdoc/require-returns-description': 'off',
-    'jsdoc/require-returns': 'off',
-  },
-
-  overrides: [
-    {
-      files: [
-        '.eslintrc.js',
-        '*.config.js',
-        '.*rc.js',
-        'ide-helper.js',
-        'config/webpack/**/*',
-        'config/formatjs-formatter.js',
-      ],
-
-      env: {
-        commonjs: true,
-      },
-
-      parserOptions: {
-        sourceType: 'script',
-      },
-
-      rules: {
-        'import/no-commonjs': 'off',
-      },
-    },
-    {
-      files: [
-        '**/*.ts',
-        '**/*.tsx',
-      ],
-
-      extends: [
-        'eslint:recommended',
-        'plugin:@typescript-eslint/strict-type-checked',
-        'plugin:@typescript-eslint/stylistic-type-checked',
-        'plugin:react/recommended',
-        'plugin:react-hooks/recommended',
-        'plugin:jsx-a11y/recommended',
-        'plugin:import/recommended',
-        'plugin:import/typescript',
-        'plugin:promise/recommended',
-        'plugin:jsdoc/recommended-typescript',
-      ],
-
-      parserOptions: {
-        projectService: true,
-        tsconfigRootDir: __dirname,
-      },
-
-      rules: {
-        // Disable formatting rules that have been enabled in the base config
-        'indent': 'off',
-
-        // This is not needed as we use noImplicitReturns, which handles this in addition to understanding types
-        'consistent-return': 'off',
-
-        'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
-
-        '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
-        '@typescript-eslint/consistent-type-exports': 'error',
-        '@typescript-eslint/consistent-type-imports': 'error',
-        "@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }],
-        "@typescript-eslint/no-restricted-imports": [
-          "warn",
-          {
-            "name": "react-redux",
-            "importNames": ["useSelector", "useDispatch"],
-            "message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
-          }
-        ],
-        "@typescript-eslint/restrict-template-expressions": ['warn', { allowNumber: true }],
-        'jsdoc/require-jsdoc': 'off',
-
-        // Those rules set stricter rules for TS files
-        // to enforce better practices when converting from JS
-        'import/no-default-export': 'warn',
-        'react/prefer-stateless-function': 'warn',
-        'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }],
-        'react/jsx-uses-react': 'off', // not needed with new JSX transform
-        'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
-        'react/prop-types': 'off',
-      },
-    },
-    {
-      files: [
-        '**/__tests__/*.js',
-        '**/__tests__/*.jsx',
-      ],
-
-      env: {
-        jest: true,
-      },
-    }
-  ],
-});
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 8a10676283..e638b9c548 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -15,6 +15,8 @@
   // to `null` after any other rule set it to something.
   dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).',
   postUpdateOptions: ['yarnDedupeHighest'],
+  // The types are now included in recent versions,we ignore them here until we upgrade and remove the dependency
+  ignoreDeps: ['@types/emoji-mart'],
   packageRules: [
     {
       // Require Dependency Dashboard Approval for major version bumps of these node packages
@@ -97,7 +99,13 @@
     {
       // Group all eslint-related packages with `eslint` in the same PR
       matchManagers: ['npm'],
-      matchPackageNames: ['eslint', 'eslint-*', '@typescript-eslint/*'],
+      matchPackageNames: [
+        'eslint',
+        'eslint-*',
+        'typescript-eslint',
+        '@eslint/*',
+        'globals',
+      ],
       matchUpdateTypes: ['patch', 'minor'],
       groupName: 'eslint (non-major)',
     },
diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml
index 1e2455d3d9..d3cb4e5e0a 100644
--- a/.github/workflows/build-security.yml
+++ b/.github/workflows/build-security.yml
@@ -24,8 +24,6 @@ jobs:
     uses: ./.github/workflows/build-container-image.yml
     with:
       file_to_build: Dockerfile
-      platforms: linux/amd64,linux/arm64
-      use_native_arm64_builder: true
       cache: false
       push_to_images: |
         tootsuite/mastodon
@@ -46,8 +44,6 @@ jobs:
     uses: ./.github/workflows/build-container-image.yml
     with:
       file_to_build: streaming/Dockerfile
-      platforms: linux/amd64,linux/arm64
-      use_native_arm64_builder: true
       cache: false
       push_to_images: |
         tootsuite/mastodon-streaming
diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml
index 1dc0800dbb..c596261eb0 100644
--- a/.github/workflows/lint-haml.yml
+++ b/.github/workflows/lint-haml.yml
@@ -46,4 +46,4 @@ jobs:
       - name: Run haml-lint
         run: |
           echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
-          bin/haml-lint --parallel --reporter github
+          bin/haml-lint --reporter github
diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml
index 621a662387..13468e7799 100644
--- a/.github/workflows/lint-js.yml
+++ b/.github/workflows/lint-js.yml
@@ -14,7 +14,7 @@ on:
       - 'tsconfig.json'
       - '.nvmrc'
       - '.prettier*'
-      - '.eslint*'
+      - 'eslint.config.mjs'
       - '**/*.js'
       - '**/*.jsx'
       - '**/*.ts'
@@ -28,7 +28,7 @@ on:
       - 'tsconfig.json'
       - '.nvmrc'
       - '.prettier*'
-      - '.eslint*'
+      - 'eslint.config.mjs'
       - '**/*.js'
       - '**/*.jsx'
       - '**/*.ts'
@@ -47,7 +47,7 @@ jobs:
         uses: ./.github/actions/setup-javascript
 
       - name: ESLint
-        run: yarn lint:js --max-warnings 0
+        run: yarn workspaces foreach --all --parallel run lint:js --max-warnings 0
 
       - name: Typecheck
         run: yarn typecheck
diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml
index 2e7123cd7e..c4a716e8f9 100644
--- a/.github/workflows/test-migrations.yml
+++ b/.github/workflows/test-migrations.yml
@@ -67,7 +67,6 @@ jobs:
       DB_HOST: localhost
       DB_USER: postgres
       DB_PASS: postgres
-      DISABLE_SIMPLECOV: true
       RAILS_ENV: test
       BUNDLE_CLEAN: true
       BUNDLE_FROZEN: true
@@ -81,6 +80,18 @@ jobs:
       - name: Set up Ruby environment
         uses: ./.github/actions/setup-ruby
 
+      - name: Ensure no errors with `db:prepare`
+        run: |
+          bin/rails db:drop
+          bin/rails db:prepare
+          bin/rails db:migrate
+
+      - name: Ensure no errors with `db:prepare` and SKIP_POST_DEPLOYMENT_MIGRATIONS
+        run: |
+          bin/rails db:drop
+          SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:prepare
+          bin/rails db:migrate
+
       - name: Test "one step migration" flow
         run: |
           bin/rails db:drop
diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml
index d52896f13c..fd4c666059 100644
--- a/.github/workflows/test-ruby.yml
+++ b/.github/workflows/test-ruby.yml
@@ -110,7 +110,7 @@ jobs:
       DB_HOST: localhost
       DB_USER: postgres
       DB_PASS: postgres
-      DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }}
+      COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }}
       RAILS_ENV: test
       ALLOW_NOPAM: true
       PAM_ENABLED: true
@@ -212,7 +212,7 @@ jobs:
       DB_HOST: localhost
       DB_USER: postgres
       DB_PASS: postgres
-      DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }}
+      COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }}
       RAILS_ENV: test
       ALLOW_NOPAM: true
       PAM_ENABLED: true
@@ -299,7 +299,6 @@ jobs:
       DB_HOST: localhost
       DB_USER: postgres
       DB_PASS: postgres
-      DISABLE_SIMPLECOV: true
       RAILS_ENV: test
       BUNDLE_WITH: test
       ES_ENABLED: false
@@ -416,7 +415,6 @@ jobs:
       DB_HOST: localhost
       DB_USER: postgres
       DB_PASS: postgres
-      DISABLE_SIMPLECOV: true
       RAILS_ENV: test
       BUNDLE_WITH: test
       ES_ENABLED: true
diff --git a/.nvmrc b/.nvmrc
index fb0a135541..744ca17ec0 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-22.13
+22.14
diff --git a/.prettierignore b/.prettierignore
index 6b2f0c1889..80b4c0159e 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -63,6 +63,7 @@ docker-compose.override.yml
 
 # Ignore emoji map file
 /app/javascript/mastodon/features/emoji/emoji_map.json
+/app/javascript/mastodon/features/emoji/emoji_sheet.json
 
 # Ignore locale files
 /app/javascript/mastodon/locales/*.json
diff --git a/.prettierrc.js b/.prettierrc.js
index af39b253f6..65ec869c33 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -1,4 +1,4 @@
 module.exports = {
   singleQuote: true,
   jsxSingleQuote: true
-}
+};
diff --git a/.rubocop.yml b/.rubocop.yml
index 342cf1dcb5..1bbba515af 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -18,6 +18,7 @@ inherit_from:
   - .rubocop/rspec_rails.yml
   - .rubocop/rspec.yml
   - .rubocop/style.yml
+  - .rubocop/i18n.yml
   - .rubocop/custom.yml
   - .rubocop_todo.yml
   - .rubocop/strict.yml
@@ -26,10 +27,10 @@ inherit_mode:
   merge:
     - Exclude
 
-require:
+plugins:
+  - rubocop-capybara
+  - rubocop-i18n
+  - rubocop-performance
   - rubocop-rails
   - rubocop-rspec
   - rubocop-rspec_rails
-  - rubocop-performance
-  - rubocop-capybara
-  - ./lib/linter/rubocop_middle_dot
diff --git a/.rubocop/i18n.yml b/.rubocop/i18n.yml
new file mode 100644
index 0000000000..de395d3a79
--- /dev/null
+++ b/.rubocop/i18n.yml
@@ -0,0 +1,12 @@
+I18n/RailsI18n:
+  Enabled: true
+  Exclude:
+    - 'config/**/*'
+    - 'db/**/*'
+    - 'lib/**/*'
+    - 'spec/**/*'
+I18n/GetText:
+  Enabled: false
+
+I18n/RailsI18n/DecorateStringFormattingUsingInterpolation:
+  Enabled: false
diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml
index ae31c1f266..bbd172e656 100644
--- a/.rubocop/rails.yml
+++ b/.rubocop/rails.yml
@@ -2,6 +2,9 @@
 Rails/BulkChangeTable:
   Enabled: false # Conflicts with strong_migrations features
 
+Rails/Delegate:
+  Enabled: false
+
 Rails/FilePath:
   EnforcedStyle: arguments
 
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 14774acde0..13fb25d333 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,6 +1,6 @@
 # This configuration was generated by
 # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
-# using RuboCop version 1.70.0.
+# using RuboCop version 1.75.2.
 # The point is for the user to remove these configuration records
 # one by one as the offenses are removed from the code base.
 # Note that changes in the inspected code, or installation of new
@@ -49,7 +49,7 @@ Style/FetchEnvVar:
     - 'lib/tasks/repo.rake'
 
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns.
+# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns.
 # SupportedStyles: annotated, template, unannotated
 # AllowedMethods: redirect
 Style/FormatStringToken:
@@ -62,22 +62,10 @@ Style/FormatStringToken:
 Style/GuardClause:
   Enabled: false
 
-# This cop supports unsafe autocorrection (--autocorrect-all).
-Style/HashTransformValues:
-  Exclude:
-    - 'app/serializers/rest/web_push_subscription_serializer.rb'
-    - 'app/services/import_service.rb'
-
-# This cop supports unsafe autocorrection (--autocorrect-all).
-Style/MapToHash:
-  Exclude:
-    - 'app/models/status.rb'
-
 # Configuration parameters: AllowedMethods.
 # AllowedMethods: respond_to_missing?
 Style/OptionalBooleanParameter:
   Exclude:
-    - 'app/helpers/json_ld_helper.rb'
     - 'app/lib/admin/system_check/message.rb'
     - 'app/lib/request.rb'
     - 'app/lib/webfinger.rb'
diff --git a/.ruby-version b/.ruby-version
index 47b322c971..6cb9d3dd0d 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.4.1
+3.4.3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef6a87ebb9..4dd4783597 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,88 @@
 
 All notable changes to this project will be documented in this file.
 
+## [4.3.7] - 2025-04-02
+
+### Add
+
+- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
+- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)
+
+### Changed
+
+- Change account suspensions to be federated to recently-followed accounts as well (#34294 by @ClearlyClaire)
+- Change `AccountReachFinder` to consider statuses based on suspension date (#32805 and #34291 by @ClearlyClaire and @mjankowski)
+- Change user archive signed URL TTL from 10 seconds to 1 hour (#34254 by @ClearlyClaire)
+
+### Fixed
+
+- Fix static version of animated PNG emojis not being properly extracted (#34337 by @ClearlyClaire)
+- Fix filters not applying in detailed view, favourites and bookmarks (#34259 and #34260 by @ClearlyClaire)
+- Fix handling of malformed/unusual HTML (#34201 by @ClearlyClaire)
+- Fix `CacheBuster` being queued for missing media attachments (#34253 by @ClearlyClaire)
+- Fix incorrect URL being used when cache busting (#34189 by @ClearlyClaire)
+- Fix streaming server refusing unix socket path in `DATABASE_URL` (#34091 by @ClearlyClaire)
+- Fix “x” hotkey not working on boosted filtered posts (#33758 by @ClearlyClaire)
+
+## [4.3.6] - 2025-03-13
+
+### Security
+
+- Update dependency `omniauth-saml`
+- Update dependency `rack`
+
+### Fixed
+
+- Fix Stoplight errors when using `REDIS_NAMESPACE` (#34126 by @ClearlyClaire)
+
+## [4.3.5] - 2025-03-10
+
+### Changed
+
+- Change hashtag suggestion to prefer personal history capitalization (#34070 by @ClearlyClaire)
+
+### Fixed
+
+- Fix processing errors for some HEIF images from iOS 18 (#34086 by @renchap)
+- Fix streaming server not filtering unknown-language posts from public timelines (#33774 by @ClearlyClaire)
+- Fix preview cards under Content Warnings not being shown in detailed statuses (#34068 by @ClearlyClaire)
+- Fix username and display name being hidden on narrow screens in moderation interface (#33064 by @ClearlyClaire)
+
+## [4.3.4] - 2025-02-27
+
+### Security
+
+- Update dependencies
+- Change HTML sanitization to remove unusable and unused `embed` tag (#34021 by @ClearlyClaire, [GHSA-mq2m-hr29-8gqf](https://github.com/mastodon/mastodon/security/advisories/GHSA-mq2m-hr29-8gqf))
+- Fix rate-limit on sign-up email verification ([GHSA-v39f-c9jj-8w7h](https://github.com/mastodon/mastodon/security/advisories/GHSA-v39f-c9jj-8w7h))
+- Fix improper disclosure of domain blocks to unverified users ([GHSA-94h4-fj37-c825](https://github.com/mastodon/mastodon/security/advisories/GHSA-94h4-fj37-c825))
+
+### Changed
+
+- Change preview cards to be shown when Content Warnings are expanded (#33827 by @ClearlyClaire)
+- Change warnings against changing encryption secrets to be even more noticeable (#33631 by @ClearlyClaire)
+- Change `mastodon:setup` to prevent overwriting already-configured servers (#33603, #33616, and #33684 by @ClearlyClaire and @mjankowski)
+- Change notifications from moderators to not be filtered (#32974 and #33654 by @ClearlyClaire and @mjankowski)
+
+### Fixed
+
+- Fix `GET /api/v2/notifications/:id` and `POST /api/v2/notifications/:id/dismiss` for ungrouped notifications (#33990 by @ClearlyClaire)
+- Fix issue with some versions of libvips on some systems (#33853 by @kleisauke)
+- Fix handling of duplicate mentions in incoming status `Update` (#33911 by @ClearlyClaire)
+- Fix inefficiencies in timeline generation (#33839 and #33842 by @ClearlyClaire)
+- Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818 by @ClearlyClaire)
+- Fix `tootctl feeds build` not building list timelines (#33783 by @ClearlyClaire)
+- Fix flaky test in `/api/v2/notifications` tests (#33773 by @ClearlyClaire)
+- Fix incorrect signature after HTTP redirect (#33757 and #33769 by @ClearlyClaire)
+- Fix polls not being validated on edition (#33755 by @ClearlyClaire)
+- Fix media preview height in compose form when 3 or more images are attached (#33571 by @ClearlyClaire)
+- Fix preview card sizing in “Author attribution” in profile settings (#33482 by @ClearlyClaire)
+- Fix processing of incoming notifications for unfilterable types (#33429 by @ClearlyClaire)
+- Fix featured tags for remote accounts not being kept up to date (#33372, #33406, and #33425 by @ClearlyClaire and @mjankowski)
+- Fix notification polling showing a loading bar in web UI (#32960 by @Gargron)
+- Fix accounts table long display name (#29316 by @WebCoder49)
+- Fix exclusive lists interfering with notifications (#28162 by @ShadowJonathan)
+
 ## [4.3.3] - 2025-01-16
 
 ### Security
diff --git a/Dockerfile b/Dockerfile
index 4e1bb24ff8..6620f4c096 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,13 +13,13 @@ ARG BASE_REGISTRY="docker.io"
 
 # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
 # renovate: datasource=docker depName=docker.io/ruby
-ARG RUBY_VERSION="3.4.1"
-# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
+ARG RUBY_VERSION="3.4.2"
+# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
 # renovate: datasource=node-version depName=node
 ARG NODE_MAJOR_VERSION="22"
 # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
 ARG DEBIAN_VERSION="bookworm"
-# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
+# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim)
 FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
 # Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm)
 FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
@@ -61,7 +61,7 @@ ENV \
 ENV \
   # Configure the IP to bind Mastodon to when serving traffic
   BIND="0.0.0.0" \
-  # Use production settings for Yarn, Node and related nodejs based tools
+  # Use production settings for Yarn, Node.js and related tools
   NODE_ENV="production" \
   # Use production settings for Ruby on Rails
   RAILS_ENV="production" \
@@ -96,6 +96,9 @@ RUN \
 # Set /opt/mastodon as working directory
 WORKDIR /opt/mastodon
 
+# Add backport repository for some specific packages where we need the latest version
+RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
+
 # hadolint ignore=DL3008,DL3005
 RUN \
   # Mount Apt cache and lib directories from Docker buildx caches
@@ -125,13 +128,6 @@ RUN \
 # Create temporary build layer from base image
 FROM ruby AS build
 
-# Copy Node package configuration files into working directory
-COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
-COPY .yarn /opt/mastodon/.yarn
-
-COPY --from=node /usr/local/bin /usr/local/bin
-COPY --from=node /usr/local/lib /usr/local/lib
-
 ARG TARGETPLATFORM
 
 # hadolint ignore=DL3008
@@ -165,7 +161,7 @@ RUN \
   libexif-dev \
   libexpat1-dev \
   libgirepository1.0-dev \
-  libheif-dev \
+  libheif-dev/bookworm-backports \
   libimagequant-dev \
   libjpeg62-turbo-dev \
   liblcms2-dev \
@@ -185,18 +181,12 @@ RUN \
   libx265-dev \
   ;
 
-RUN \
-  # Configure Corepack
-  rm /usr/local/bin/yarn*; \
-  corepack enable; \
-  corepack prepare --activate;
-
 # Create temporary libvips specific build layer from build layer
 FROM build AS libvips
 
 # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
 # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
-ARG VIPS_VERSION=8.16.0
+ARG VIPS_VERSION=8.16.1
 # libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
 ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
 
@@ -281,38 +271,37 @@ RUN \
   # Download and install required Gems
   bundle install -j"$(nproc)";
 
-# Create temporary node specific build layer from build layer
-FROM build AS yarn
+# Create temporary assets build layer from build layer
+FROM build AS precompiler
 
 ARG TARGETPLATFORM
 
-# Copy Node package configuration files into working directory
-COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
-COPY streaming/package.json /opt/mastodon/streaming/
-COPY .yarn /opt/mastodon/.yarn
+# Copy Mastodon sources into layer
+COPY . /opt/mastodon/
+
+# Copy Node.js binaries/libraries into layer
+COPY --from=node /usr/local/bin /usr/local/bin
+COPY --from=node /usr/local/lib /usr/local/lib
+
+RUN \
+  # Configure Corepack
+  rm /usr/local/bin/yarn*; \
+  corepack enable; \
+  corepack prepare --activate;
 
 # hadolint ignore=DL3008
 RUN \
   --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
   --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
-  # Install Node packages
+  # Install Node.js packages
   yarn workspaces focus --production @mastodon/mastodon;
 
-# Create temporary assets build layer from build layer
-FROM build AS precompiler
-
-# Copy Mastodon sources into precompiler layer
-COPY . /opt/mastodon/
-
-# Copy bundler and node packages from build layer to container
-COPY --from=yarn /opt/mastodon /opt/mastodon/
-COPY --from=bundler /opt/mastodon /opt/mastodon/
-COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
-# Copy libvips components to layer for precompiler
+# Copy libvips components into layer for precompiler
 COPY --from=libvips /usr/local/libvips/bin /usr/local/bin
 COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
-
-ARG TARGETPLATFORM
+# Copy bundler packages into layer for precompiler
+COPY --from=bundler /opt/mastodon /opt/mastodon/
+COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
 
 RUN \
   ldconfig; \
@@ -348,7 +337,7 @@ RUN \
   # libvips components
   libcgif0 \
   libexif12 \
-  libheif1 \
+  libheif1/bookworm-backports \
   libimagequant0 \
   libjpeg62-turbo \
   liblcms2-2 \
diff --git a/Gemfile b/Gemfile
index 89648e8cac..9e5955e0b8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -14,6 +14,7 @@ gem 'haml-rails', '~>2.0'
 gem 'pg', '~> 1.5'
 gem 'pghero'
 
+gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873
 gem 'aws-sdk-s3', '~> 1.123', require: false
 gem 'blurhash', '~> 0.1'
 gem 'fog-core', '<= 2.6.0'
@@ -39,7 +40,7 @@ gem 'net-ldap', '~> 0.18'
 
 gem 'omniauth', '~> 2.0'
 gem 'omniauth-cas', '~> 3.0.0.beta.1'
-gem 'omniauth_openid_connect', '~> 0.6.1'
+gem 'omniauth_openid_connect', '~> 0.8.0'
 gem 'omniauth-rails_csrf_protection', '~> 1.0'
 gem 'omniauth-saml', '~> 2.0'
 
@@ -61,6 +62,7 @@ gem 'inline_svg'
 gem 'irb', '~> 1.8'
 gem 'kaminari', '~> 1.2'
 gem 'link_header', '~> 0.0'
+gem 'linzer', '~> 0.6.1'
 gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
 gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar'
 gem 'mutex_m'
@@ -102,10 +104,10 @@ gem 'rdf-normalize', '~> 0.5'
 
 gem 'prometheus_exporter', '~> 2.2', require: false
 
-gem 'opentelemetry-api', '~> 1.4.0'
+gem 'opentelemetry-api', '~> 1.5.0'
 
 group :opentelemetry do
-  gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false
+  gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false
   gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
   gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
   gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
@@ -116,7 +118,7 @@ group :opentelemetry do
   gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false
   gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
   gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false
-  gem 'opentelemetry-instrumentation-rails', '~> 0.35.0', require: false
+  gem 'opentelemetry-instrumentation-rails', '~> 0.36.0', require: false
   gem 'opentelemetry-instrumentation-redis', '~> 0.26.0', require: false
   gem 'opentelemetry-instrumentation-sidekiq', '~> 0.26.0', require: false
   gem 'opentelemetry-sdk', '~> 1.4', require: false
@@ -145,9 +147,6 @@ group :test do
   # Used to mock environment variables
   gem 'climate_control'
 
-  # Add back helpers functions removed in Rails 5.1
-  gem 'rails-controller-testing', '~> 1.0'
-
   # Validate schemas in specs
   gem 'json-schema', '~> 5.0'
 
@@ -156,7 +155,7 @@ group :test do
 
   gem 'shoulda-matchers'
 
-  # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false
+  # Coverage formatter for RSpec
   gem 'simplecov', '~> 0.22', require: false
   gem 'simplecov-lcov', '~> 0.8', require: false
 
@@ -168,13 +167,14 @@ group :development do
   # Code linting CLI and plugins
   gem 'rubocop', require: false
   gem 'rubocop-capybara', require: false
+  gem 'rubocop-i18n', require: false
   gem 'rubocop-performance', require: false
   gem 'rubocop-rails', require: false
   gem 'rubocop-rspec', require: false
   gem 'rubocop-rspec_rails', require: false
 
   # Annotates modules with schema
-  gem 'annotaterb', '~> 4.13'
+  gem 'annotaterb', '~> 4.13', require: false
 
   # Enhanced error message pages for development
   gem 'better_errors', '~> 2.9'
@@ -197,7 +197,7 @@ end
 
 group :development, :test do
   # Interactive Debugging tools
-  gem 'debug', '~> 1.8'
+  gem 'debug', '~> 1.8', require: false
 
   # Generate fake data values
   gem 'faker', '~> 3.2'
@@ -209,7 +209,7 @@ group :development, :test do
   gem 'memory_profiler', require: false
   gem 'ruby-prof', require: false
   gem 'stackprof', require: false
-  gem 'test-prof'
+  gem 'test-prof', require: false
 
   # RSpec runner for rails
   gem 'rspec-rails', '~> 7.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 8547e4fba1..f13df0c43f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -10,29 +10,29 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (8.0.1)
-      actionpack (= 8.0.1)
-      activesupport (= 8.0.1)
+    actioncable (8.0.2)
+      actionpack (= 8.0.2)
+      activesupport (= 8.0.2)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
       zeitwerk (~> 2.6)
-    actionmailbox (8.0.1)
-      actionpack (= 8.0.1)
-      activejob (= 8.0.1)
-      activerecord (= 8.0.1)
-      activestorage (= 8.0.1)
-      activesupport (= 8.0.1)
+    actionmailbox (8.0.2)
+      actionpack (= 8.0.2)
+      activejob (= 8.0.2)
+      activerecord (= 8.0.2)
+      activestorage (= 8.0.2)
+      activesupport (= 8.0.2)
       mail (>= 2.8.0)
-    actionmailer (8.0.1)
-      actionpack (= 8.0.1)
-      actionview (= 8.0.1)
-      activejob (= 8.0.1)
-      activesupport (= 8.0.1)
+    actionmailer (8.0.2)
+      actionpack (= 8.0.2)
+      actionview (= 8.0.2)
+      activejob (= 8.0.2)
+      activesupport (= 8.0.2)
       mail (>= 2.8.0)
       rails-dom-testing (~> 2.2)
-    actionpack (8.0.1)
-      actionview (= 8.0.1)
-      activesupport (= 8.0.1)
+    actionpack (8.0.2)
+      actionview (= 8.0.2)
+      activesupport (= 8.0.2)
       nokogiri (>= 1.8.5)
       rack (>= 2.2.4)
       rack-session (>= 1.0.1)
@@ -40,15 +40,15 @@ GEM
       rails-dom-testing (~> 2.2)
       rails-html-sanitizer (~> 1.6)
       useragent (~> 0.16)
-    actiontext (8.0.1)
-      actionpack (= 8.0.1)
-      activerecord (= 8.0.1)
-      activestorage (= 8.0.1)
-      activesupport (= 8.0.1)
+    actiontext (8.0.2)
+      actionpack (= 8.0.2)
+      activerecord (= 8.0.2)
+      activestorage (= 8.0.2)
+      activesupport (= 8.0.2)
       globalid (>= 0.6.0)
       nokogiri (>= 1.8.5)
-    actionview (8.0.1)
-      activesupport (= 8.0.1)
+    actionview (8.0.2)
+      activesupport (= 8.0.2)
       builder (~> 3.1)
       erubi (~> 1.11)
       rails-dom-testing (~> 2.2)
@@ -58,22 +58,22 @@ GEM
       activemodel (>= 4.1)
       case_transform (>= 0.2)
       jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
-    activejob (8.0.1)
-      activesupport (= 8.0.1)
+    activejob (8.0.2)
+      activesupport (= 8.0.2)
       globalid (>= 0.3.6)
-    activemodel (8.0.1)
-      activesupport (= 8.0.1)
-    activerecord (8.0.1)
-      activemodel (= 8.0.1)
-      activesupport (= 8.0.1)
+    activemodel (8.0.2)
+      activesupport (= 8.0.2)
+    activerecord (8.0.2)
+      activemodel (= 8.0.2)
+      activesupport (= 8.0.2)
       timeout (>= 0.4.0)
-    activestorage (8.0.1)
-      actionpack (= 8.0.1)
-      activejob (= 8.0.1)
-      activerecord (= 8.0.1)
-      activesupport (= 8.0.1)
+    activestorage (8.0.2)
+      actionpack (= 8.0.2)
+      activejob (= 8.0.2)
+      activerecord (= 8.0.2)
+      activesupport (= 8.0.2)
       marcel (~> 1.0)
-    activesupport (8.0.1)
+    activesupport (8.0.2)
       base64
       benchmark (>= 0.3)
       bigdecimal
@@ -90,12 +90,12 @@ GEM
       public_suffix (>= 2.0.2, < 7.0)
     aes_key_wrap (1.1.0)
     android_key_attestation (0.3.0)
-    annotaterb (4.13.0)
-    ast (2.4.2)
+    annotaterb (4.14.0)
+    ast (2.4.3)
     attr_required (1.0.2)
-    aws-eventstream (1.3.0)
-    aws-partitions (1.1032.0)
-    aws-sdk-core (3.214.1)
+    aws-eventstream (1.3.2)
+    aws-partitions (1.1087.0)
+    aws-sdk-core (3.215.1)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
@@ -107,9 +107,9 @@ GEM
       aws-sdk-core (~> 3, >= 3.210.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
-    aws-sigv4 (1.10.1)
+    aws-sigv4 (1.11.0)
       aws-eventstream (~> 1, >= 1.0.2)
-    azure-blob (0.5.4)
+    azure-blob (0.5.7)
       rexml
     base64 (0.2.0)
     bcp47_spec (0.2.1)
@@ -120,13 +120,13 @@ GEM
       rack (>= 0.9.0)
       rouge (>= 1.0.0)
     bigdecimal (3.1.9)
-    bindata (2.5.0)
+    bindata (2.5.1)
     binding_of_caller (1.0.1)
       debug_inspector (>= 1.2.0)
     blurhash (0.1.8)
     bootsnap (1.18.4)
       msgpack (~> 1.2)
-    brakeman (7.0.0)
+    brakeman (7.0.2)
       racc
     browser (6.2.0)
     brpoplpush-redis_script (0.1.3)
@@ -168,9 +168,9 @@ GEM
       bigdecimal
       rexml
     crass (1.0.6)
-    css_parser (1.21.0)
+    css_parser (1.21.1)
       addressable
-    csv (3.3.2)
+    csv (3.3.4)
     database_cleaner-active_record (2.2.0)
       activerecord (>= 5.a)
       database_cleaner-core (~> 2.0.0)
@@ -194,14 +194,14 @@ GEM
     devise_pam_authenticatable2 (9.2.0)
       devise (>= 4.0.0)
       rpam2 (~> 4.0)
-    diff-lcs (1.5.1)
+    diff-lcs (1.6.1)
     discard (1.4.0)
       activerecord (>= 4.2, < 9.0)
     docile (1.4.1)
     domain_name (0.6.20240107)
-    doorkeeper (5.8.1)
+    doorkeeper (5.8.2)
       railties (>= 5)
-    dotenv (3.1.7)
+    dotenv (3.1.8)
     drb (2.2.1)
     elasticsearch (7.17.11)
       elasticsearch-api (= 7.17.11)
@@ -217,24 +217,29 @@ GEM
       htmlentities (~> 4.3.3)
       launchy (>= 2.1, < 4.0)
       mail (~> 2.7)
+    email_validator (2.2.4)
+      activemodel
     erubi (1.13.1)
     et-orbi (1.2.11)
       tzinfo
-    excon (0.112.0)
+    excon (1.2.5)
+      logger
     fabrication (2.31.0)
     faker (3.5.1)
       i18n (>= 1.8.11, < 2)
-    faraday (2.12.2)
+    faraday (2.13.0)
       faraday-net_http (>= 2.0, < 3.5)
       json
       logger
+    faraday-follow_redirects (0.3.0)
+      faraday (>= 1, < 3)
     faraday-httpclient (2.0.1)
       httpclient (>= 2.2)
     faraday-net_http (3.4.0)
       net-http (>= 0.5.0)
     fast_blank (1.0.1)
     fastimage (2.4.0)
-    ffi (1.17.1)
+    ffi (1.17.2)
     ffi-compiler (1.3.2)
       ffi (>= 1.15.5)
       rake
@@ -244,15 +249,15 @@ GEM
     flatware-rspec (2.3.4)
       flatware (= 2.3.4)
       rspec (>= 3.6)
-    fog-core (2.5.0)
+    fog-core (2.6.0)
       builder
-      excon (~> 0.71)
+      excon (~> 1.0)
       formatador (>= 0.2, < 2.0)
       mime-types
     fog-json (1.2.0)
       fog-core
       multi_json (~> 1.10)
-    fog-openstack (1.1.3)
+    fog-openstack (1.1.5)
       fog-core (~> 2.1)
       fog-json (>= 1.0)
     formatador (1.1.0)
@@ -261,8 +266,10 @@ GEM
       raabro (~> 1.4)
     globalid (1.2.1)
       activesupport (>= 6.1)
-    google-protobuf (3.25.5)
-    googleapis-common-protos-types (1.15.0)
+    google-protobuf (4.30.2)
+      bigdecimal
+      rake (>= 13)
+    googleapis-common-protos-types (1.19.0)
       google-protobuf (>= 3.18, < 5.a)
     haml (6.3.0)
       temple (>= 0.8.2)
@@ -273,7 +280,7 @@ GEM
       activesupport (>= 5.1)
       haml (>= 4.0.6)
       railties (>= 5.1)
-    haml_lint (0.59.0)
+    haml_lint (0.62.0)
       haml (>= 5.0)
       parallel (~> 1.10)
       rainbow
@@ -298,13 +305,14 @@ GEM
       domain_name (~> 0.5)
     http-form_data (2.3.0)
     http_accept_language (2.1.1)
-    httpclient (2.8.3)
+    httpclient (2.9.0)
+      mutex_m
     httplog (1.7.0)
       rack (>= 2.0)
       rainbow (>= 2.0.0)
     i18n (1.14.7)
       concurrent-ruby (~> 1.0)
-    i18n-tasks (1.0.14)
+    i18n-tasks (1.0.15)
       activesupport (>= 4.0.2)
       ast (>= 2.1.0)
       erubi
@@ -313,13 +321,14 @@ GEM
       parser (>= 3.2.2.1)
       rails-i18n
       rainbow (>= 2.2.2, < 4.0)
+      ruby-progressbar (~> 1.8, >= 1.8.1)
       terminal-table (>= 1.5.1)
     idn-ruby (0.1.5)
     inline_svg (1.10.0)
       activesupport (>= 3.0)
       nokogiri (>= 1.6)
     io-console (0.8.0)
-    irb (1.15.1)
+    irb (1.15.2)
       pp (>= 0.6.0)
       rdoc (>= 4.0.0)
       reline (>= 0.4.2)
@@ -328,13 +337,15 @@ GEM
       azure-blob (~> 0.5.2)
       hashie (~> 5.0)
     jmespath (1.6.2)
-    json (2.9.1)
+    json (2.10.2)
     json-canonicalization (1.0.0)
-    json-jwt (1.15.3.1)
+    json-jwt (1.16.7)
       activesupport (>= 4.2)
       aes_key_wrap
+      base64
       bindata
-      httpclient
+      faraday (~> 2.0)
+      faraday-follow_redirects
     json-ld (3.3.2)
       htmlentities (~> 4.3)
       json-canonicalization (~> 1.0)
@@ -350,7 +361,7 @@ GEM
       addressable (~> 2.8)
       bigdecimal (~> 3.1)
     jsonapi-renderer (0.2.2)
-    jwt (2.9.3)
+    jwt (2.10.1)
       base64
     kaminari (1.2.2)
       activesupport (>= 4.1.0)
@@ -371,9 +382,10 @@ GEM
       mime-types
       terrapin (>= 0.6.0, < 2.0)
     language_server-protocol (3.17.0.4)
-    launchy (3.0.1)
+    launchy (3.1.1)
       addressable (~> 2.8)
       childprocess (~> 5.0)
+      logger (~> 1.6)
     letter_opener (1.10.0)
       launchy (>= 2.2, < 4)
     letter_opener_web (3.0.0)
@@ -382,10 +394,17 @@ GEM
       railties (>= 6.1)
       rexml
     link_header (0.0.8)
-    llhttp-ffi (0.5.0)
+    lint_roller (1.1.0)
+    linzer (0.6.5)
+      openssl (~> 3.0, >= 3.0.0)
+      rack (>= 2.2, < 4.0)
+      starry (~> 0.2)
+      stringio (~> 3.1, >= 3.1.2)
+      uri (~> 1.0, >= 1.0.2)
+    llhttp-ffi (0.5.1)
       ffi-compiler (~> 1.0)
       rake (~> 13.0)
-    logger (1.6.5)
+    logger (1.7.0)
     lograge (0.14.0)
       actionpack (>= 4)
       activesupport (>= 4)
@@ -404,19 +423,19 @@ GEM
       redis (>= 3.0.5)
     matrix (0.4.2)
     memory_profiler (1.1.0)
-    mime-types (3.6.0)
+    mime-types (3.6.2)
       logger
       mime-types-data (~> 3.2015)
-    mime-types-data (3.2025.0107)
+    mime-types-data (3.2025.0408)
     mini_mime (1.1.5)
     mini_portile2 (2.8.8)
-    minitest (5.25.4)
-    msgpack (1.7.5)
+    minitest (5.25.5)
+    msgpack (1.8.0)
     multi_json (1.15.0)
     mutex_m (0.3.0)
     net-http (0.6.0)
       uri
-    net-imap (0.5.5)
+    net-imap (0.5.6)
       date
       net-protocol
     net-ldap (0.19.0)
@@ -424,50 +443,52 @@ GEM
       net-protocol
     net-protocol (0.2.2)
       timeout
-    net-smtp (0.5.0)
+    net-smtp (0.5.1)
       net-protocol
     nio4r (2.7.4)
-    nokogiri (1.18.2)
+    nokogiri (1.18.7)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    oj (3.16.9)
+    oj (3.16.10)
       bigdecimal (>= 3.0)
       ostruct (>= 0.2)
-    omniauth (2.1.2)
+    omniauth (2.1.3)
       hashie (>= 3.4.6)
       rack (>= 2.2.3)
       rack-protection
-    omniauth-cas (3.0.0)
+    omniauth-cas (3.0.1)
       addressable (~> 2.8)
       nokogiri (~> 1.12)
       omniauth (~> 2.1)
     omniauth-rails_csrf_protection (1.0.2)
       actionpack (>= 4.2)
       omniauth (~> 2.0)
-    omniauth-saml (2.2.1)
+    omniauth-saml (2.2.3)
       omniauth (~> 2.1)
-      ruby-saml (~> 1.17)
-    omniauth_openid_connect (0.6.1)
+      ruby-saml (~> 1.18)
+    omniauth_openid_connect (0.8.0)
       omniauth (>= 1.9, < 3)
-      openid_connect (~> 1.1)
-    openid_connect (1.4.2)
+      openid_connect (~> 2.2)
+    openid_connect (2.3.1)
       activemodel
       attr_required (>= 1.0.0)
-      json-jwt (>= 1.15.0)
-      net-smtp
-      rack-oauth2 (~> 1.21)
-      swd (~> 1.3)
+      email_validator
+      faraday (~> 2.0)
+      faraday-follow_redirects
+      json-jwt (>= 1.16)
+      mail
+      rack-oauth2 (~> 2.2)
+      swd (~> 2.0)
       tzinfo
-      validate_email
       validate_url
-      webfinger (~> 1.2)
-    openssl (3.2.1)
+      webfinger (~> 2.0)
+    openssl (3.3.0)
     openssl-signature_algorithm (1.3.0)
       openssl (> 2.0)
-    opentelemetry-api (1.4.0)
-    opentelemetry-common (0.21.0)
+    opentelemetry-api (1.5.0)
+    opentelemetry-common (0.22.0)
       opentelemetry-api (~> 1.0)
-    opentelemetry-exporter-otlp (0.29.1)
+    opentelemetry-exporter-otlp (0.30.0)
       google-protobuf (>= 3.18)
       googleapis-common-protos-types (~> 1.3)
       opentelemetry-api (~> 1.1)
@@ -480,7 +501,7 @@ GEM
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-active_support (~> 0.7)
       opentelemetry-instrumentation-base (~> 0.23.0)
-    opentelemetry-instrumentation-action_pack (0.11.0)
+    opentelemetry-instrumentation-action_pack (0.12.0)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.23.0)
       opentelemetry-instrumentation-rack (~> 0.21)
@@ -498,6 +519,10 @@ GEM
     opentelemetry-instrumentation-active_record (0.9.0)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.23.0)
+    opentelemetry-instrumentation-active_storage (0.1.1)
+      opentelemetry-api (~> 1.0)
+      opentelemetry-instrumentation-active_support (~> 0.7)
+      opentelemetry-instrumentation-base (~> 0.23.0)
     opentelemetry-instrumentation-active_support (0.8.0)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.23.0)
@@ -530,44 +555,45 @@ GEM
     opentelemetry-instrumentation-rack (0.26.0)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.23.0)
-    opentelemetry-instrumentation-rails (0.35.1)
+    opentelemetry-instrumentation-rails (0.36.0)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-action_mailer (~> 0.4.0)
-      opentelemetry-instrumentation-action_pack (~> 0.11.0)
+      opentelemetry-instrumentation-action_pack (~> 0.12.0)
       opentelemetry-instrumentation-action_view (~> 0.9.0)
       opentelemetry-instrumentation-active_job (~> 0.8.0)
       opentelemetry-instrumentation-active_record (~> 0.9.0)
+      opentelemetry-instrumentation-active_storage (~> 0.1.0)
       opentelemetry-instrumentation-active_support (~> 0.8.0)
       opentelemetry-instrumentation-base (~> 0.23.0)
       opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
-    opentelemetry-instrumentation-redis (0.26.0)
+    opentelemetry-instrumentation-redis (0.26.1)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.23.0)
-    opentelemetry-instrumentation-sidekiq (0.26.0)
+    opentelemetry-instrumentation-sidekiq (0.26.1)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.23.0)
-    opentelemetry-registry (0.3.1)
+    opentelemetry-registry (0.4.0)
       opentelemetry-api (~> 1.1)
-    opentelemetry-sdk (1.6.0)
+    opentelemetry-sdk (1.8.0)
       opentelemetry-api (~> 1.1)
       opentelemetry-common (~> 0.20)
       opentelemetry-registry (~> 0.2)
       opentelemetry-semantic_conventions
-    opentelemetry-semantic_conventions (1.10.1)
+    opentelemetry-semantic_conventions (1.11.0)
       opentelemetry-api (~> 1.0)
     orm_adapter (0.5.0)
     ostruct (0.6.1)
-    ox (2.14.21)
+    ox (2.14.22)
       bigdecimal (>= 3.0)
-    parallel (1.26.3)
-    parser (3.3.7.0)
+    parallel (1.27.0)
+    parser (3.3.8.0)
       ast (~> 2.4.1)
       racc
     parslet (2.0.0)
     pastel (0.8.0)
       tty-color (~> 0.5)
     pg (1.5.9)
-    pghero (3.6.1)
+    pghero (3.6.2)
       activerecord (>= 6.1)
     pp (0.6.2)
       prettyprint
@@ -580,6 +606,7 @@ GEM
       net-smtp
       premailer (~> 1.7, >= 1.7.9)
     prettyprint (0.2.0)
+    prism (1.4.0)
     prometheus_exporter (2.2.0)
       webrick
     propshaft (1.1.0)
@@ -591,21 +618,22 @@ GEM
       date
       stringio
     public_suffix (6.0.1)
-    puma (6.5.0)
+    puma (6.6.0)
       nio4r (~> 2.0)
-    pundit (2.4.0)
+    pundit (2.5.0)
       activesupport (>= 3.0.0)
     raabro (1.4.0)
     racc (1.8.1)
-    rack (2.2.10)
+    rack (2.2.13)
     rack-attack (6.7.0)
       rack (>= 1.0, < 4)
     rack-cors (2.0.2)
       rack (>= 2.0.0)
-    rack-oauth2 (1.21.3)
+    rack-oauth2 (2.2.1)
       activesupport
       attr_required
-      httpclient
+      faraday (~> 2.0)
+      faraday-follow_redirects
       json-jwt (>= 1.11.0)
       rack (>= 2.1.0)
     rack-protection (3.2.0)
@@ -620,24 +648,20 @@ GEM
     rackup (1.0.1)
       rack (< 3)
       webrick
-    rails (8.0.1)
-      actioncable (= 8.0.1)
-      actionmailbox (= 8.0.1)
-      actionmailer (= 8.0.1)
-      actionpack (= 8.0.1)
-      actiontext (= 8.0.1)
-      actionview (= 8.0.1)
-      activejob (= 8.0.1)
-      activemodel (= 8.0.1)
-      activerecord (= 8.0.1)
-      activestorage (= 8.0.1)
-      activesupport (= 8.0.1)
+    rails (8.0.2)
+      actioncable (= 8.0.2)
+      actionmailbox (= 8.0.2)
+      actionmailer (= 8.0.2)
+      actionpack (= 8.0.2)
+      actiontext (= 8.0.2)
+      actionview (= 8.0.2)
+      activejob (= 8.0.2)
+      activemodel (= 8.0.2)
+      activerecord (= 8.0.2)
+      activestorage (= 8.0.2)
+      activesupport (= 8.0.2)
       bundler (>= 1.15.0)
-      railties (= 8.0.1)
-    rails-controller-testing (1.0.5)
-      actionpack (>= 5.0.1.rc1)
-      actionview (>= 5.0.1.rc1)
-      activesupport (>= 5.0.1.rc1)
+      railties (= 8.0.2)
     rails-dom-testing (2.2.0)
       activesupport (>= 5.0.0)
       minitest
@@ -648,9 +672,9 @@ GEM
     rails-i18n (8.0.1)
       i18n (>= 0.7, < 2)
       railties (>= 8.0.0, < 9)
-    railties (8.0.1)
-      actionpack (= 8.0.1)
-      activesupport (= 8.0.1)
+    railties (8.0.2)
+      actionpack (= 8.0.2)
+      activesupport (= 8.0.2)
       irb (~> 1.13)
       rackup (>= 1.0.0)
       rake (>= 12.2)
@@ -664,23 +688,23 @@ GEM
       link_header (~> 0.0, >= 0.0.8)
     rdf-normalize (0.7.0)
       rdf (~> 3.3)
-    rdoc (6.11.0)
+    rdoc (6.13.1)
       psych (>= 4.0.0)
-    redcarpet (3.6.0)
+    redcarpet (3.6.1)
     redis (4.8.1)
     redis-namespace (1.11.0)
       redis (>= 4)
     redlock (1.3.2)
       redis (>= 3.0.0, < 6.0)
     regexp_parser (2.10.0)
-    reline (0.6.0)
+    reline (0.6.1)
       io-console (~> 0.5)
     request_store (1.7.0)
       rack (>= 1.4)
     responders (3.1.1)
       actionpack (>= 5.2)
       railties (>= 5.2)
-    rexml (3.4.0)
+    rexml (3.4.1)
     rotp (6.3.0)
     rouge (4.5.1)
     rpam2 (4.0.2)
@@ -692,7 +716,7 @@ GEM
       rspec-core (~> 3.13.0)
       rspec-expectations (~> 3.13.0)
       rspec-mocks (~> 3.13.0)
-    rspec-core (3.13.2)
+    rspec-core (3.13.3)
       rspec-support (~> 3.13.0)
     rspec-expectations (3.13.3)
       diff-lcs (>= 1.2.0, < 2.0)
@@ -702,7 +726,7 @@ GEM
     rspec-mocks (3.13.2)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.13.0)
-    rspec-rails (7.1.0)
+    rspec-rails (7.1.1)
       actionpack (>= 7.0)
       activesupport (>= 7.0)
       railties (>= 7.0)
@@ -710,45 +734,55 @@ GEM
       rspec-expectations (~> 3.13)
       rspec-mocks (~> 3.13)
       rspec-support (~> 3.13)
-    rspec-sidekiq (5.0.0)
+    rspec-sidekiq (5.1.0)
       rspec-core (~> 3.0)
       rspec-expectations (~> 3.0)
       rspec-mocks (~> 3.0)
-      sidekiq (>= 5, < 8)
+      sidekiq (>= 5, < 9)
     rspec-support (3.13.2)
-    rubocop (1.71.0)
+    rubocop (1.75.2)
       json (~> 2.3)
-      language_server-protocol (>= 3.17.0)
+      language_server-protocol (~> 3.17.0.2)
+      lint_roller (~> 1.1.0)
       parallel (~> 1.10)
       parser (>= 3.3.0.2)
       rainbow (>= 2.2.2, < 4.0)
       regexp_parser (>= 2.9.3, < 3.0)
-      rubocop-ast (>= 1.36.2, < 2.0)
+      rubocop-ast (>= 1.44.0, < 2.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (>= 2.4.0, < 4.0)
-    rubocop-ast (1.38.0)
-      parser (>= 3.3.1.0)
-    rubocop-capybara (2.21.0)
-      rubocop (~> 1.41)
-    rubocop-performance (1.23.1)
-      rubocop (>= 1.48.1, < 2.0)
-      rubocop-ast (>= 1.31.1, < 2.0)
-    rubocop-rails (2.29.1)
+    rubocop-ast (1.44.1)
+      parser (>= 3.3.7.2)
+      prism (~> 1.4)
+    rubocop-capybara (2.22.1)
+      lint_roller (~> 1.1)
+      rubocop (~> 1.72, >= 1.72.1)
+    rubocop-i18n (3.2.3)
+      lint_roller (~> 1.1)
+      rubocop (>= 1.72.1)
+    rubocop-performance (1.25.0)
+      lint_roller (~> 1.1)
+      rubocop (>= 1.75.0, < 2.0)
+      rubocop-ast (>= 1.38.0, < 2.0)
+    rubocop-rails (2.31.0)
       activesupport (>= 4.2.0)
+      lint_roller (~> 1.1)
       rack (>= 1.1)
-      rubocop (>= 1.52.0, < 2.0)
-      rubocop-ast (>= 1.31.1, < 2.0)
-    rubocop-rspec (3.4.0)
-      rubocop (~> 1.61)
-    rubocop-rspec_rails (2.30.0)
-      rubocop (~> 1.61)
-      rubocop-rspec (~> 3, >= 3.0.1)
+      rubocop (>= 1.75.0, < 2.0)
+      rubocop-ast (>= 1.38.0, < 2.0)
+    rubocop-rspec (3.5.0)
+      lint_roller (~> 1.1)
+      rubocop (~> 1.72, >= 1.72.1)
+    rubocop-rspec_rails (2.31.0)
+      lint_roller (~> 1.1)
+      rubocop (~> 1.72, >= 1.72.1)
+      rubocop-rspec (~> 3.5)
     ruby-prof (1.7.1)
     ruby-progressbar (1.13.0)
-    ruby-saml (1.17.0)
+    ruby-saml (1.18.0)
       nokogiri (>= 1.13.10)
       rexml
-    ruby-vips (2.2.2)
+    ruby-vips (2.2.3)
       ffi (~> 1.12)
       logger
     rubyzip (2.4.1)
@@ -763,7 +797,7 @@ GEM
       activerecord (>= 4.0.0)
       railties (>= 4.0.0)
     securerandom (0.4.1)
-    selenium-webdriver (4.28.0)
+    selenium-webdriver (4.31.0)
       base64 (~> 0.2)
       logger (~> 1.4)
       rexml (~> 3.2, >= 3.2.5)
@@ -801,26 +835,29 @@ GEM
     simplecov-lcov (0.8.0)
     simplecov_json_formatter (0.1.4)
     stackprof (0.2.27)
-    stoplight (4.1.0)
+    starry (0.2.0)
+      base64
+    stoplight (4.1.1)
       redlock (~> 1.0)
-    stringio (3.1.2)
-    strong_migrations (2.1.0)
-      activerecord (>= 6.1)
-    swd (1.3.0)
+    stringio (3.1.6)
+    strong_migrations (2.3.0)
+      activerecord (>= 7)
+    swd (2.0.3)
       activesupport (>= 3)
       attr_required (>= 0.0.5)
-      httpclient (>= 2.4)
+      faraday (~> 2.0)
+      faraday-follow_redirects
     sysexits (1.2.0)
     temple (0.10.3)
-    terminal-table (3.0.2)
-      unicode-display_width (>= 1.1.1, < 3)
-    terrapin (1.0.1)
+    terminal-table (4.0.0)
+      unicode-display_width (>= 1.1.1, < 4)
+    terrapin (1.1.0)
       climate_control
     test-prof (1.4.4)
     thor (1.3.2)
-    tilt (2.5.0)
+    tilt (2.6.0)
     timeout (0.4.3)
-    tpm-key_attestation (0.12.1)
+    tpm-key_attestation (0.14.0)
       bindata (~> 2.4)
       openssl (> 2.0)
       openssl-signature_algorithm (~> 1.0)
@@ -839,34 +876,34 @@ GEM
       unf (~> 0.1.0)
     tzinfo (2.0.6)
       concurrent-ruby (~> 1.0)
-    tzinfo-data (1.2025.1)
+    tzinfo-data (1.2025.2)
       tzinfo (>= 1.0.0)
     unf (0.1.4)
       unf_ext
     unf_ext (0.0.9.1)
-    unicode-display_width (2.6.0)
-    uri (1.0.2)
+    unicode-display_width (3.1.4)
+      unicode-emoji (~> 4.0, >= 4.0.4)
+    unicode-emoji (4.0.4)
+    uri (1.0.3)
     useragent (0.16.11)
-    validate_email (0.1.6)
-      activemodel (>= 3.0)
-      mail (>= 2.2.5)
     validate_url (1.0.15)
       activemodel (>= 3.0.0)
       public_suffix
     warden (1.2.9)
       rack (>= 2.0.9)
-    webauthn (3.2.2)
+    webauthn (3.4.0)
       android_key_attestation (~> 0.3.0)
       bindata (~> 2.4)
       cbor (~> 0.5.9)
       cose (~> 1.1)
       openssl (>= 2.2)
       safety_net_attestation (~> 0.4.0)
-      tpm-key_attestation (~> 0.12.0)
-    webfinger (1.2.0)
+      tpm-key_attestation (~> 0.14.0)
+    webfinger (2.1.3)
       activesupport
-      httpclient (>= 2.4)
-    webmock (3.24.0)
+      faraday (~> 2.0)
+      faraday-follow_redirects
+    webmock (3.25.1)
       addressable (>= 2.8.0)
       crack (>= 0.3.2)
       hashdiff (>= 0.4.0, < 2.0.0)
@@ -885,7 +922,7 @@ GEM
     xorcist (1.1.3)
     xpath (3.2.0)
       nokogiri (~> 1.8)
-    zeitwerk (2.7.1)
+    zeitwerk (2.7.2)
 
 PLATFORMS
   ruby
@@ -894,6 +931,7 @@ DEPENDENCIES
   active_model_serializers (~> 0.10)
   addressable (~> 2.8)
   annotaterb (~> 4.13)
+  aws-sdk-core (< 3.216.0)
   aws-sdk-s3 (~> 1.123)
   better_errors (~> 2.9)
   binding_of_caller (~> 1.0)
@@ -950,6 +988,7 @@ DEPENDENCIES
   letter_opener (~> 1.8)
   letter_opener_web (~> 3.0)
   link_header (~> 0.0)
+  linzer (~> 0.6.1)
   lograge (~> 0.12)
   mail (~> 2.8)
   mario-redis-lock (~> 1.2)
@@ -964,9 +1003,9 @@ DEPENDENCIES
   omniauth-cas (~> 3.0.0.beta.1)
   omniauth-rails_csrf_protection (~> 1.0)
   omniauth-saml (~> 2.0)
-  omniauth_openid_connect (~> 0.6.1)
-  opentelemetry-api (~> 1.4.0)
-  opentelemetry-exporter-otlp (~> 0.29.0)
+  omniauth_openid_connect (~> 0.8.0)
+  opentelemetry-api (~> 1.5.0)
+  opentelemetry-exporter-otlp (~> 0.30.0)
   opentelemetry-instrumentation-active_job (~> 0.8.0)
   opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
   opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
@@ -977,7 +1016,7 @@ DEPENDENCIES
   opentelemetry-instrumentation-net_http (~> 0.23.0)
   opentelemetry-instrumentation-pg (~> 0.30.0)
   opentelemetry-instrumentation-rack (~> 0.26.0)
-  opentelemetry-instrumentation-rails (~> 0.35.0)
+  opentelemetry-instrumentation-rails (~> 0.36.0)
   opentelemetry-instrumentation-redis (~> 0.26.0)
   opentelemetry-instrumentation-sidekiq (~> 0.26.0)
   opentelemetry-sdk (~> 1.4)
@@ -996,7 +1035,6 @@ DEPENDENCIES
   rack-cors (~> 2.0)
   rack-test (~> 2.1)
   rails (~> 8.0)
-  rails-controller-testing (~> 1.0)
   rails-i18n (~> 8.0)
   rdf-normalize (~> 0.5)
   redcarpet (~> 3.6)
@@ -1008,6 +1046,7 @@ DEPENDENCIES
   rspec-sidekiq (~> 5.0)
   rubocop
   rubocop-capybara
+  rubocop-i18n
   rubocop-performance
   rubocop-rails
   rubocop-rspec
@@ -1046,4 +1085,4 @@ RUBY VERSION
    ruby 3.4.1p0
 
 BUNDLED WITH
-   2.6.3
+   2.6.8
diff --git a/README.md b/README.md
index 200d58d8c4..854e8ac3d9 100644
--- a/README.md
+++ b/README.md
@@ -1,123 +1,27 @@
-# ![kmyblue icon](https://raw.githubusercontent.com/kmycode/mastodon/kb_development/app/javascript/icons/favicon-32x32.png) kmyblue
+NAS is an KMY & Mastodon Fork
 
-[![Ruby Testing](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml)
+The following are just a few of the most common features. There are many other minor changes to the specifications.
 
-! FOR ENGLISH USER ! We do not provide English documentation for kmyblue; we assume that you will use automatic translation software, such as Google, to translate the site.
+Emoji reactions
 
-kmyblueは、ActivityPubに接続するSNSの1つである[Mastodon](https://github.com/mastodon/mastodon)のフォークです。創作作家のためのMastodonを目指して開発しました。
+Local Public (Does not appear on the federated timeline of remote servers, but does appear on followers' home timelines. This is different from local only)
 
-kmyblueはフォーク名であり、同時に[サーバー名](https://kmy.blue)でもあります。以下は特に記述がない限り、フォークとしてのkmyblueをさします。
+Bookmark classification
 
-kmyblueは AGPL ライセンスで公開されているため、どなたでも自由にフォークし、このソースコードを元に自分でサーバーを立てて公開することができます。確かにサーバーkmyblueは創作作家向けの利用規約が設定されていますが、フォークとしてのkmyblueのルールは全くの別物です。いかなるコミュニティにも平等にお使いいただけます。  
-kmyblueは、閉鎖的なコミュニティ、あまり目立ちたくないコミュニティには特に強力な機能を提供します。kmyblueはプライバシーを考慮したうえで強力な独自機能を提供するため、汎用サーバーとして利用するにもある程度十分な機能が揃っています。
+Set who can search your posts for each post (Searchability)
 
-テストコード、Lint どちらも動いています。
+Quote posts, modest quotes (references)
 
-### アジェンダ
+Record posts that meet certain conditions such as domains, accounts, and keywords (Subscriptions/Antennas)
 
-- 利用方法
-- kmyblueの開発方針
-- kmyblueは何でないか
-- kmyblueの独自機能
-- 英語のサポートについて
+Send posts to a designated set of followers (Circles) (different from direct messages)
 
-## 利用方法
+Notification of new posts on lists
 
-### インストール方法
+Exclude posts from people you follow when filtering posts
 
-[Wiki](https://github.com/kmycode/mastodon/wiki/Installation)を参照してください。
+Hide number of followers and followings
 
-### 開発への参加方法
+Automatically delete posts after a specified time has passed
 
-CONTRIBUTING.mdを参照してください。
-
-### テスト
-
-```
-# デバッグ実行(以下のいずれか)
-foreman start
-DB_USER=postgres DB_PASS=password foreman start
-
-# 一部を除く全てのテストを行う
-RAILS_ENV=test bundle exec rspec spec
-
-# ElasticSearch連携テストを行う
-新
-RAILS_ENV=test ES_ENABLED=true bundle exec rspec --tag search
-旧
-RAILS_ENV=test ES_ENABLED=true RUN_SEARCH_SPECS=true bundle exec rspec spec/search
-```
-
-## kmyblueの開発方針
-
-### 本家Mastodonへの積極的追従
-
-kmyblueは、追加機能を控えめにする代わりに本家Mastodonに積極的に追従を行います。kmyblueの追加機能そのままに、Mastodonの新機能も利用できるよう調整を行います。
-
-### ゆるやかな内輪での運用
-
-kmyblueは同人向けサーバーとして出発したため、同人作家に需要のある「内輪ノリを外部にできるだけもらさない」という部分に特化しています。
-
-「ローカル公開」は、投稿を見せたくない人に見つかりにくくする効果があります。「サークル」は、フォロワーの中でも特に見せたい人だけに見せる効果があります。  
-「検索許可」という独自の検索オプションを利用することで、公開投稿の一部だけを検索されにくくするだけでなく、非収載投稿が誰でも自由に検索できるようになります。
-
-内輪とは自分のサーバーに限ったものではありません。内輪同士で複数のサーバーを運営するとき、お互いが深く繋がれる「フレンドサーバー」というシステムも用意しています。
-
-### 少人数サーバーでの運用
-
-kmyblueは、人の少ないサーバーでの運用を考慮して設計しています。そのため、Fedibirdにあるような、人の多いサーバー向けの機能はあまり作っていません。
-
-サーバーの負荷については一部度外視している部分があります。たとえば絵文字リアクション機能はサーバーへ著しい負荷をかける場合があります。ただしkmyblueでは、絵文字リアクション機能そのものを無効にしたり、負荷の高いストリーミング処理を無効にする管理者オプションも存在します。
-
-もちろん人の多いサーバーでの運用が不便になるような修正は行っていません。人数にかかわらず、そのままお使いいただけます。
-
-### 比較的高い防御力
-
-kmyblueでは、「Fediverseは将来的に荒むのではないか」「Fediverseは将来的にスパムに溢れるのではないか」を念頭に設計している部分があります。投稿だけでなく絵文字リアクションも対象にした防衛策があります。
-
-管理者は「NGワード」「NGルール」機能の利用が可能です。設定を変更することで、一部のモデレーターもこの機能を利用できます。  
-利用者は、独自拡張されたフィルター機能、絵文字リアクションのブロックなどを利用できます。
-
-ただし防御力の高さは自由を犠牲にします。例えばNGワードが多すぎると、他のサーバーからの投稿が制限され、かつそれに気づきにくくなります。
-
-## kmyblueは何でないか
-
-kmyblueは、企業・政府機関向けに開発されたものではありません。開発者はセキュリティに関する専門知識を有しておらず、高度なセキュリティを求められる機関向けのソフトウェアを制作する能力はありません。また、kmyblueのメンテナは現在1人のみであり、そのメンテナが飽きたら開発がストップするリスクも高いです。Mastodonのような高い信頼性・安全性を保証することはできないので、導入の際はご自身で安全を十分に確認してからお使いになることを強くおすすめします。  
-個人サーバーであっても、安定性を強く求める方にはおすすめできません。glitch-socがよりよい選択肢になるでしょう。
-
-kmyblueは、Misskeyではありません。Misskeyは「楽しむ」をコンセプトにしていますが、kmyblueはMastodonの思想を受け継ぎ、炎上や喧騒を避けることのできる落ち着いた場所を目指しています。そのため、思想に合わない機能は実装しないか、大幅に弱体化しています。
-
-kmyblueは、Fedibirdではありません。Fedibirdは大規模サーバー向けに設定していると思われる機能があり、例えば購読機能がその代表例です。Fedibirdの購読は擬似的なフォロー体験を与えるものですが、本物のフォローではないため、購読対象の投稿が配送されることを確約したものではありません。小規模サーバーだとかえって不便になる機能を、kmyblueは避けています。
-
-## kmyblueの独自機能
-
-以下に列挙したものはあくまで代表的なものです。これ以外にも、細かい仕様変更などが多数含まれます。
-
-- 絵文字リアクション
-- ローカル公開(Local Public)(リモートサーバーの連合タイムラインには流れませんが、フォロワーのホームタイムラインには流れます。**ローカル限定とは異なります**)
-- ブックマークの分類
-- 自分の投稿を検索できる人を投稿ごとに設定(検索許可・Searchability)
-- 投稿の引用、ひかえめな引用(参照)
-- ドメイン・アカウント・キーワードなど特定条件を満たした投稿を記録する機能(購読・アンテナ)
-- フォロワーの一部を指名して投稿を送る機能(サークル)(ダイレクトメッセージとは異なります)
-- リスト新着投稿の通知
-- 投稿のフィルタリングにおいて、自分がフォローしている相手の投稿を除外
-- フォロー・フォロワー数を隠す機能
-- 指定した時間が経過したあとに投稿を自動削除する機能
-- モデレーション機能の拡張
-
-## 英語のサポートについて
-
-kmyblueのメイン開発者である[雪あすか](https://kmy.blue/@askyq)は、英語の読み書きがほとんどできません。そのため、ドキュメントの英語化、海外向け公式アカウントの新設などを行う予定はありません。
-
-要望やバグ報告はIssueに書いて構いませんが、Issue画面内の説明やテンプレートはすべて日本語になっています。投稿が難しければ、Discussionに投稿してください。こちらで必要と判断したものは、改めてIssueとして起票します。
-
-そのほか開発者へ質問があれば、[@askyq@kmy.blue](https://kmy.blue/@askyq)へ英語のまま送ってください。
-
-ただしkmyblueのドキュメント、[@askyq@kmy.blue](https://kmy.blue/@askyq)内のkmyblueフォークに関係する投稿を、許可なく翻訳して公開することは問題ありません。
-
-## 開発者のアカウントについて
-
-kmyblueのメイン開発者である[雪あすか](https://kmy.blue/@askyq)は、用途別にアカウントを分けるようなことはせず、すべての発言を1つのアカウントで行っています。そのため、kmyblueの開発だけでなく、成人向け同人作品の話も混ざっています。
-
-このうち、公開範囲「公開」「ローカル公開」「非収載」であるkmyblueフォークの開発に関係する投稿に限り抽出し、翻訳の有無に関係なく公開することを許可します。これはkmyblueフォークの利用者にとって公共性の高いコンテンツであると思われます。これは、日本と欧米では一般的に考えられている児童ポルノの基準が異なり、欧米のサーバーの中にはこのアカウントをフォローしづらいものもあるという懸念を考慮したものです。
+Expanding moderation functions
diff --git a/app/controllers/admin/announcements/distributions_controller.rb b/app/controllers/admin/announcements/distributions_controller.rb
new file mode 100644
index 0000000000..4bd8769834
--- /dev/null
+++ b/app/controllers/admin/announcements/distributions_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Admin::Announcements::DistributionsController < Admin::BaseController
+  before_action :set_announcement
+
+  def create
+    authorize @announcement, :distribute?
+    @announcement.touch(:notification_sent_at)
+    Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id)
+    redirect_to admin_announcements_path
+  end
+
+  private
+
+  def set_announcement
+    @announcement = Announcement.find(params[:announcement_id])
+  end
+end
diff --git a/app/controllers/admin/announcements/previews_controller.rb b/app/controllers/admin/announcements/previews_controller.rb
new file mode 100644
index 0000000000..d77f931a7f
--- /dev/null
+++ b/app/controllers/admin/announcements/previews_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Admin::Announcements::PreviewsController < Admin::BaseController
+  before_action :set_announcement
+
+  def show
+    authorize @announcement, :distribute?
+    @user_count = @announcement.scope_for_notification.count
+  end
+
+  private
+
+  def set_announcement
+    @announcement = Announcement.find(params[:announcement_id])
+  end
+end
diff --git a/app/controllers/admin/announcements/tests_controller.rb b/app/controllers/admin/announcements/tests_controller.rb
new file mode 100644
index 0000000000..f2457eb23a
--- /dev/null
+++ b/app/controllers/admin/announcements/tests_controller.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class Admin::Announcements::TestsController < Admin::BaseController
+  before_action :set_announcement
+
+  def create
+    authorize @announcement, :distribute?
+    UserMailer.announcement_published(current_user, @announcement).deliver_later!
+    redirect_to admin_announcements_path
+  end
+
+  private
+
+  def set_announcement
+    @announcement = Announcement.find(params[:announcement_id])
+  end
+end
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index 3dca3a9614..14338dd293 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -7,17 +7,12 @@ module Admin
 
     layout 'admin'
 
-    before_action :set_cache_headers
     before_action :set_referrer_policy_header
 
     after_action :verify_authorized
 
     private
 
-    def set_cache_headers
-      response.cache_control.replace(private: true, no_store: true)
-    end
-
     def set_referrer_policy_header
       response.headers['Referrer-Policy'] = 'same-origin'
     end
diff --git a/app/controllers/admin/fasp/debug/callbacks_controller.rb b/app/controllers/admin/fasp/debug/callbacks_controller.rb
new file mode 100644
index 0000000000..28aba5e489
--- /dev/null
+++ b/app/controllers/admin/fasp/debug/callbacks_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Admin::Fasp::Debug::CallbacksController < Admin::BaseController
+  def index
+    authorize [:admin, :fasp, :provider], :update?
+
+    @callbacks = Fasp::DebugCallback
+                 .includes(:fasp_provider)
+                 .order(created_at: :desc)
+  end
+
+  def destroy
+    authorize [:admin, :fasp, :provider], :update?
+
+    callback = Fasp::DebugCallback.find(params[:id])
+    callback.destroy
+
+    redirect_to admin_fasp_debug_callbacks_path
+  end
+end
diff --git a/app/controllers/admin/fasp/debug_calls_controller.rb b/app/controllers/admin/fasp/debug_calls_controller.rb
new file mode 100644
index 0000000000..1e1b6dbf3c
--- /dev/null
+++ b/app/controllers/admin/fasp/debug_calls_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class Admin::Fasp::DebugCallsController < Admin::BaseController
+  before_action :set_provider
+
+  def create
+    authorize [:admin, @provider], :update?
+
+    @provider.perform_debug_call
+
+    redirect_to admin_fasp_providers_path
+  end
+
+  private
+
+  def set_provider
+    @provider = Fasp::Provider.find(params[:provider_id])
+  end
+end
diff --git a/app/controllers/admin/fasp/providers_controller.rb b/app/controllers/admin/fasp/providers_controller.rb
new file mode 100644
index 0000000000..4f1f1271bf
--- /dev/null
+++ b/app/controllers/admin/fasp/providers_controller.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class Admin::Fasp::ProvidersController < Admin::BaseController
+  before_action :set_provider, only: [:show, :edit, :update, :destroy]
+
+  def index
+    authorize [:admin, :fasp, :provider], :index?
+
+    @providers = Fasp::Provider.order(confirmed: :asc, created_at: :desc)
+  end
+
+  def show
+    authorize [:admin, @provider], :show?
+  end
+
+  def edit
+    authorize [:admin, @provider], :update?
+  end
+
+  def update
+    authorize [:admin, @provider], :update?
+
+    if @provider.update(provider_params)
+      redirect_to admin_fasp_providers_path
+    else
+      render :edit
+    end
+  end
+
+  def destroy
+    authorize [:admin, @provider], :destroy?
+
+    @provider.destroy
+
+    redirect_to admin_fasp_providers_path
+  end
+
+  private
+
+  def provider_params
+    params.expect(fasp_provider: [capabilities_attributes: {}])
+  end
+
+  def set_provider
+    @provider = Fasp::Provider.find(params[:id])
+  end
+end
diff --git a/app/controllers/admin/fasp/registrations_controller.rb b/app/controllers/admin/fasp/registrations_controller.rb
new file mode 100644
index 0000000000..52c46c2eb6
--- /dev/null
+++ b/app/controllers/admin/fasp/registrations_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Admin::Fasp::RegistrationsController < Admin::BaseController
+  before_action :set_provider
+
+  def new
+    authorize [:admin, @provider], :create?
+  end
+
+  def create
+    authorize [:admin, @provider], :create?
+
+    @provider.update_info!(confirm: true)
+
+    redirect_to edit_admin_fasp_provider_path(@provider)
+  end
+
+  private
+
+  def set_provider
+    @provider = Fasp::Provider.find(params[:provider_id])
+  end
+end
diff --git a/app/controllers/admin/ng_words/keywords_controller.rb b/app/controllers/admin/ng_words/keywords_controller.rb
index 9af38fab7b..10969204e8 100644
--- a/app/controllers/admin/ng_words/keywords_controller.rb
+++ b/app/controllers/admin/ng_words/keywords_controller.rb
@@ -21,6 +21,10 @@ module Admin
       false
     end
 
+    def avoid_save?
+      true
+    end
+
     private
 
     def after_update_redirect_path
diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb
index f052843475..9e437f8c8b 100644
--- a/app/controllers/admin/ng_words_controller.rb
+++ b/app/controllers/admin/ng_words_controller.rb
@@ -13,6 +13,12 @@ module Admin
 
       return unless validate
 
+      if avoid_save?
+        flash[:notice] = I18n.t('generic.changes_saved_msg')
+        redirect_to after_update_redirect_path
+        return
+      end
+
       @admin_settings = Form::AdminSettings.new(settings_params)
 
       if @admin_settings.save
@@ -33,6 +39,10 @@ module Admin
       admin_ng_words_path
     end
 
+    def avoid_save?
+      false
+    end
+
     private
 
     def settings_params
@@ -40,7 +50,7 @@ module Admin
     end
 
     def settings_params_test
-      params.require(:form_admin_settings)[:ng_words_test]
+      params.expect(form_admin_settings: [ng_words_test: [keywords: [], regexps: [], strangers: [], temporary_ids: []]])['ng_words_test']
     end
   end
 end
diff --git a/app/controllers/admin/software_updates_controller.rb b/app/controllers/admin/software_updates_controller.rb
index d7b114a100..c9be97eb71 100644
--- a/app/controllers/admin/software_updates_controller.rb
+++ b/app/controllers/admin/software_updates_controller.rb
@@ -6,7 +6,7 @@ module Admin
 
     def index
       authorize :software_update, :index?
-      @software_updates = SoftwareUpdate.by_version
+      @software_updates = SoftwareUpdate.by_version.filter(&:pending?)
     end
 
     private
diff --git a/app/controllers/admin/terms_of_service/drafts_controller.rb b/app/controllers/admin/terms_of_service/drafts_controller.rb
index 02cb05946f..0c67eb9df8 100644
--- a/app/controllers/admin/terms_of_service/drafts_controller.rb
+++ b/app/controllers/admin/terms_of_service/drafts_controller.rb
@@ -23,7 +23,7 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
   private
 
   def set_terms_of_service
-    @terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text)
+    @terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now)
   end
 
   def current_terms_of_service
@@ -32,6 +32,6 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
 
   def resource_params
     params
-      .expect(terms_of_service: [:text, :changelog])
+      .expect(terms_of_service: [:text, :changelog, :effective_date])
   end
 end
diff --git a/app/controllers/admin/terms_of_service_controller.rb b/app/controllers/admin/terms_of_service_controller.rb
index f70bfd2071..10aa5c66ca 100644
--- a/app/controllers/admin/terms_of_service_controller.rb
+++ b/app/controllers/admin/terms_of_service_controller.rb
@@ -3,6 +3,6 @@
 class Admin::TermsOfServiceController < Admin::BaseController
   def index
     authorize :terms_of_service, :index?
-    @terms_of_service = TermsOfService.live.first
+    @terms_of_service = TermsOfService.published.first
   end
 end
diff --git a/app/controllers/api/fasp/base_controller.rb b/app/controllers/api/fasp/base_controller.rb
new file mode 100644
index 0000000000..690f7e419a
--- /dev/null
+++ b/app/controllers/api/fasp/base_controller.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+class Api::Fasp::BaseController < ApplicationController
+  class Error < ::StandardError; end
+
+  DIGEST_PATTERN = /sha-256=:(.*?):/
+  KEYID_PATTERN = /keyid="(.*?)"/
+
+  attr_reader :current_provider
+
+  skip_forgery_protection
+
+  before_action :check_fasp_enabled
+  before_action :require_authentication
+  after_action :sign_response
+
+  private
+
+  def require_authentication
+    validate_content_digest!
+    validate_signature!
+  rescue Error, Linzer::Error, ActiveRecord::RecordNotFound => e
+    logger.debug("FASP Authentication error: #{e}")
+    authentication_error
+  end
+
+  def authentication_error
+    respond_to do |format|
+      format.json { head 401 }
+    end
+  end
+
+  def validate_content_digest!
+    content_digest_header = request.headers['content-digest']
+    raise Error, 'content-digest missing' if content_digest_header.blank?
+
+    digest_received = content_digest_header.match(DIGEST_PATTERN)[1]
+
+    digest_computed = OpenSSL::Digest.base64digest('sha256', request.body&.string || '')
+
+    raise Error, 'content-digest does not match' if digest_received != digest_computed
+  end
+
+  def validate_signature!
+    signature_input = request.headers['signature-input']&.encode('UTF-8')
+    raise Error, 'signature-input is missing' if signature_input.blank?
+
+    keyid = signature_input.match(KEYID_PATTERN)[1]
+    provider = Fasp::Provider.find(keyid)
+    linzer_request = Linzer.new_request(
+      request.method,
+      request.original_url,
+      {},
+      {
+        'content-digest' => request.headers['content-digest'],
+        'signature-input' => signature_input,
+        'signature' => request.headers['signature'],
+      }
+    )
+    message = Linzer::Message.new(linzer_request)
+    key = Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid)
+    signature = Linzer::Signature.build(message.headers)
+    Linzer.verify(key, message, signature)
+    @current_provider = provider
+  end
+
+  def sign_response
+    response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:"
+
+    linzer_response = Linzer.new_response(response.body, response.status, { 'content-digest' => response.headers['content-digest'] })
+    message = Linzer::Message.new(linzer_response)
+    key = Linzer.new_ed25519_key(current_provider.server_private_key_pem)
+    signature = Linzer.sign(key, message, %w(@status content-digest))
+
+    response.headers.merge!(signature.to_h)
+  end
+
+  def check_fasp_enabled
+    raise ActionController::RoutingError unless Mastodon::Feature.fasp_enabled?
+  end
+end
diff --git a/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb b/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb
new file mode 100644
index 0000000000..794e53f095
--- /dev/null
+++ b/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Api::Fasp::Debug::V0::Callback::ResponsesController < Api::Fasp::BaseController
+  def create
+    Fasp::DebugCallback.create(
+      fasp_provider: current_provider,
+      ip: request.remote_ip,
+      request_body: request.raw_post
+    )
+
+    respond_to do |format|
+      format.json { head 201 }
+    end
+  end
+end
diff --git a/app/controllers/api/fasp/registrations_controller.rb b/app/controllers/api/fasp/registrations_controller.rb
new file mode 100644
index 0000000000..fecc992fec
--- /dev/null
+++ b/app/controllers/api/fasp/registrations_controller.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class Api::Fasp::RegistrationsController < Api::Fasp::BaseController
+  skip_before_action :require_authentication
+
+  def create
+    @current_provider = Fasp::Provider.create!(
+      name: params[:name],
+      base_url: params[:baseUrl],
+      remote_identifier: params[:serverId],
+      provider_public_key_base64: params[:publicKey]
+    )
+
+    render json: registration_confirmation
+  end
+
+  private
+
+  def registration_confirmation
+    {
+      faspId: current_provider.id.to_s,
+      publicKey: current_provider.server_public_key_base64,
+      registrationCompletionUri: new_admin_fasp_provider_registration_url(current_provider),
+    }
+  end
+end
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 032e42e9d2..bdd7732b87 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
     @account = current_account
     UpdateAccountService.new.call(@account, account_params, raise_error: true)
     current_user.update(user_params) if user_params
-    ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+    ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
     render json: @account, serializer: REST::CredentialAccountSerializer
   rescue ActiveRecord::RecordInvalid => e
     render json: ValidationErrorFormatter.new(e).as_json, status: 422
diff --git a/app/controllers/api/v1/accounts/endorsements_controller.rb b/app/controllers/api/v1/accounts/endorsements_controller.rb
new file mode 100644
index 0000000000..1e21994a90
--- /dev/null
+++ b/app/controllers/api/v1/accounts/endorsements_controller.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+class Api::V1::Accounts::EndorsementsController < Api::BaseController
+  include Authorization
+
+  before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :index
+  before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
+  before_action :require_user!, except: :index
+  before_action :set_account
+  before_action :set_endorsed_accounts, only: :index
+  after_action :insert_pagination_headers, only: :index
+
+  def index
+    cache_if_unauthenticated!
+    render json: @endorsed_accounts, each_serializer: REST::AccountSerializer
+  end
+
+  def create
+    AccountPin.find_or_create_by!(account: current_account, target_account: @account)
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
+  end
+
+  def destroy
+    pin = AccountPin.find_by(account: current_account, target_account: @account)
+    pin&.destroy!
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
+  end
+
+  private
+
+  def set_account
+    @account = Account.find(params[:account_id])
+  end
+
+  def set_endorsed_accounts
+    @endorsed_accounts = @account.unavailable? ? [] : paginated_endorsed_accounts
+  end
+
+  def paginated_endorsed_accounts
+    @account.endorsed_accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id(
+      limit_param(DEFAULT_ACCOUNTS_LIMIT),
+      params[:max_id],
+      params[:since_id]
+    )
+  end
+
+  def relationships_presenter
+    AccountRelationshipsPresenter.new([@account], current_user.account_id)
+  end
+
+  def next_path
+    api_v1_account_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue?
+  end
+
+  def prev_path
+    api_v1_account_endorsements_url pagination_params(since_id: pagination_since_id) unless @endorsed_accounts.empty?
+  end
+
+  def pagination_collection
+    @endorsed_accounts
+  end
+
+  def records_continue?
+    @endorsed_accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
+  end
+end
diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb
index 0101fb469b..f95846366c 100644
--- a/app/controllers/api/v1/accounts/featured_tags_controller.rb
+++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb
@@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
   end
 
   def set_featured_tags
-    @featured_tags = @account.suspended? ? [] : @account.featured_tags
+    @featured_tags = @account.unavailable? ? [] : @account.featured_tags
   end
 end
diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb
index 48f293f47a..02a45e8758 100644
--- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb
+++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb
@@ -1,6 +1,10 @@
 # frozen_string_literal: true
 
 class Api::V1::Accounts::IdentityProofsController < Api::BaseController
+  include DeprecationConcern
+
+  deprecate_api '2022-03-30'
+
   before_action :require_user!
   before_action :set_account
 
diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb
deleted file mode 100644
index 0eb13c048c..0000000000
--- a/app/controllers/api/v1/accounts/pins_controller.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-class Api::V1::Accounts::PinsController < Api::BaseController
-  include Authorization
-
-  before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
-  before_action :require_user!
-  before_action :set_account
-
-  def create
-    AccountPin.find_or_create_by!(account: current_account, target_account: @account)
-    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
-  end
-
-  def destroy
-    pin = AccountPin.find_by(account: current_account, target_account: @account)
-    pin&.destroy!
-    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
-  end
-
-  private
-
-  def set_account
-    @account = Account.find(params[:account_id])
-  end
-
-  def relationships_presenter
-    AccountRelationshipsPresenter.new([@account], current_user.account_id)
-  end
-end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 6bef6a3768..46838aeb66 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -124,7 +124,7 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def account_params
-    params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
+    params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth)
   end
 
   def invite
diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb
index c97e9720ad..f8d91c5f7f 100644
--- a/app/controllers/api/v1/filters_controller.rb
+++ b/app/controllers/api/v1/filters_controller.rb
@@ -1,6 +1,10 @@
 # frozen_string_literal: true
 
 class Api::V1::FiltersController < Api::BaseController
+  include DeprecationConcern
+
+  deprecate_api '2022-11-14'
+
   before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
   before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
   before_action :require_user!
diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb
index 7ec94312f4..bf96fbaaa8 100644
--- a/app/controllers/api/v1/instances/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb
@@ -31,7 +31,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
   end
 
   def show_domain_blocks_to_user?
-    Setting.show_domain_blocks == 'users' && user_signed_in?
+    Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved?
   end
 
   def set_domain_blocks
@@ -47,6 +47,6 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
   end
 
   def show_rationale_for_user?
-    Setting.show_domain_blocks_rationale == 'users' && user_signed_in?
+    Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved?
   end
 end
diff --git a/app/controllers/api/v1/instances/terms_of_services_controller.rb b/app/controllers/api/v1/instances/terms_of_services_controller.rb
index e9e8e8ef55..0a861dd7bb 100644
--- a/app/controllers/api/v1/instances/terms_of_services_controller.rb
+++ b/app/controllers/api/v1/instances/terms_of_services_controller.rb
@@ -5,12 +5,18 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo
 
   def show
     cache_even_if_authenticated!
-    render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
+    render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer
   end
 
   private
 
   def set_terms_of_service
-    @terms_of_service = TermsOfService.live.first!
+    @terms_of_service = begin
+      if params[:date].present?
+        TermsOfService.published.find_by!(effective_date: params[:date])
+      else
+        TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet
+      end
+    end
   end
 end
diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb
index 49da75ed28..e01267c000 100644
--- a/app/controllers/api/v1/instances_controller.rb
+++ b/app/controllers/api/v1/instances_controller.rb
@@ -1,15 +1,9 @@
 # frozen_string_literal: true
 
-class Api::V1::InstancesController < Api::BaseController
-  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
-  skip_around_action :set_locale
+class Api::V1::InstancesController < Api::V2::InstancesController
+  include DeprecationConcern
 
-  vary_by ''
-
-  # Override `current_user` to avoid reading session cookies unless in limited federation mode
-  def current_user
-    super if limited_federation_mode?
-  end
+  deprecate_api '2022-11-14'
 
   def show
     cache_even_if_authenticated!
diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb
index 2086bf116d..b019ab6018 100644
--- a/app/controllers/api/v1/lists_controller.rb
+++ b/app/controllers/api/v1/lists_controller.rb
@@ -7,10 +7,6 @@ class Api::V1::ListsController < Api::BaseController
   before_action :require_user!
   before_action :set_list, except: [:index, :create]
 
-  rescue_from ArgumentError do |e|
-    render json: { error: e.to_s }, status: 422
-  end
-
   def index
     @lists = List.where(account: current_account).all
     render json: @lists, each_serializer: REST::ListSerializer
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index 5ea26d55bd..c427e055ea 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -3,8 +3,8 @@
 class Api::V1::MediaController < Api::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:media' }
   before_action :require_user!
-  before_action :set_media_attachment, except: [:create]
-  before_action :check_processing, except: [:create]
+  before_action :set_media_attachment, except: [:create, :destroy]
+  before_action :check_processing, except: [:create, :destroy]
 
   def show
     render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
@@ -25,6 +25,15 @@ class Api::V1::MediaController < Api::BaseController
     render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
   end
 
+  def destroy
+    @media_attachment = current_account.media_attachments.find(params[:id])
+
+    return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil?
+
+    @media_attachment.destroy
+    render_empty
+  end
+
   private
 
   def status_code_for_media_attachment
@@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController
   def processing_error
     { error: 'Error processing thumbnail for uploaded media' }
   end
+
+  def in_usage_error
+    { error: 'Media attachment is currently used by a status' }
+  end
 end
diff --git a/app/controllers/api/v1/profile/avatars_controller.rb b/app/controllers/api/v1/profile/avatars_controller.rb
index bc4d01a597..e6c954ed63 100644
--- a/app/controllers/api/v1/profile/avatars_controller.rb
+++ b/app/controllers/api/v1/profile/avatars_controller.rb
@@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController
   def destroy
     @account = current_account
     UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
-    ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+    ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
     render json: @account, serializer: REST::CredentialAccountSerializer
   end
 end
diff --git a/app/controllers/api/v1/profile/headers_controller.rb b/app/controllers/api/v1/profile/headers_controller.rb
index 9f4daa2f77..4472a01b05 100644
--- a/app/controllers/api/v1/profile/headers_controller.rb
+++ b/app/controllers/api/v1/profile/headers_controller.rb
@@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController
   def destroy
     @account = current_account
     UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
-    ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+    ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
     render json: @account, serializer: REST::CredentialAccountSerializer
   end
 end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 534347d019..1217b70752 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -67,6 +67,8 @@ class Api::V1::StatusesController < Api::BaseController
     statuses = [@status] + @context.ancestors + @context.descendants + @context.references
 
     render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
+
+    ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies?
   end
 
   def create
@@ -125,7 +127,7 @@ class Api::V1::StatusesController < Api::BaseController
     @status.account.statuses_count = @status.account.statuses_count - 1
     json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
 
-    RemovalWorker.perform_async(@status.id, { 'redraft' => true })
+    RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
 
     render json: json
   end
diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb
index 9ba1cef63c..df9346832f 100644
--- a/app/controllers/api/v1/suggestions_controller.rb
+++ b/app/controllers/api/v1/suggestions_controller.rb
@@ -2,6 +2,9 @@
 
 class Api::V1::SuggestionsController < Api::BaseController
   include Authorization
+  include DeprecationConcern
+
+  deprecate_api '2021-05-16', only: [:index]
 
   before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
   before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb
index 10a3442344..ecac3579fc 100644
--- a/app/controllers/api/v1/trends/tags_controller.rb
+++ b/app/controllers/api/v1/trends/tags_controller.rb
@@ -1,11 +1,15 @@
 # frozen_string_literal: true
 
 class Api::V1::Trends::TagsController < Api::BaseController
+  include DeprecationConcern
+
   before_action :set_tags
 
   after_action :insert_pagination_headers
 
-  DEFAULT_TAGS_LIMIT = 10
+  DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i
+
+  deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' }
 
   def index
     cache_if_unauthenticated!
diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb
index 8346e28830..62adf95260 100644
--- a/app/controllers/api/v2/instances_controller.rb
+++ b/app/controllers/api/v2/instances_controller.rb
@@ -1,6 +1,16 @@
 # frozen_string_literal: true
 
-class Api::V2::InstancesController < Api::V1::InstancesController
+class Api::V2::InstancesController < Api::BaseController
+  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
+  skip_around_action :set_locale
+
+  vary_by ''
+
+  # Override `current_user` to avoid reading session cookies unless in limited federation mode
+  def current_user
+    super if limited_federation_mode?
+  end
+
   def show
     cache_even_if_authenticated!
     render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'
diff --git a/app/controllers/api/v2/notifications_controller.rb b/app/controllers/api/v2/notifications_controller.rb
index cc38b95114..848c361cfc 100644
--- a/app/controllers/api/v2/notifications_controller.rb
+++ b/app/controllers/api/v2/notifications_controller.rb
@@ -46,7 +46,7 @@ class Api::V2::NotificationsController < Api::BaseController
   end
 
   def show
-    @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key])
+    @notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take!
     presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
     render json: presenter, serializer: REST::DedupNotificationGroupSerializer
   end
@@ -57,7 +57,7 @@ class Api::V2::NotificationsController < Api::BaseController
   end
 
   def dismiss
-    current_account.notifications.where(group_key: params[:group_key]).destroy_all
+    current_account.notifications.by_group_key(params[:group_key]).destroy_all
     render_empty
   end
 
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 34c7599553..0b6f5b3af4 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -12,7 +12,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   before_action :set_sessions, only: [:edit, :update]
   before_action :set_strikes, only: [:edit, :update]
   before_action :require_not_suspended!, only: [:update]
-  before_action :set_cache_headers, only: [:edit, :update]
   before_action :set_rules, only: :new
   before_action :require_rules_acceptance!, only: :new
   before_action :set_registration_form_time, only: :new
@@ -63,7 +62,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   def configure_sign_up_params
     devise_parameter_sanitizer.permit(:sign_up) do |user_params|
-      user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
+      user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth)
     end
   end
 
@@ -139,10 +138,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
     set_locale { render :rules }
   end
 
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
-
   def is_flashing_format? # rubocop:disable Naming/PredicateName
     if params[:action] == 'create'
       false # Disable flash messages for sign-up
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index eb8ac38da9..5f9f133659 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -174,7 +174,7 @@ class Auth::SessionsController < Devise::SessionsController
   end
 
   def disable_custom_css?
-    user_params[:disable_css].present? && user_params[:disable_css] != '0'
+    user_params[:disable_css].present? && user_params[:disable_css] == '1'
   end
 
   def disable_custom_css!(user)
diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb
index 5df1af5f2f..076d19874b 100644
--- a/app/controllers/backups_controller.rb
+++ b/app/controllers/backups_controller.rb
@@ -9,13 +9,15 @@ class BackupsController < ApplicationController
   before_action :authenticate_user!
   before_action :set_backup
 
+  BACKUP_LINK_TIMEOUT = 1.hour.freeze
+
   def download
     case Paperclip::Attachment.default_options[:storage]
     when :s3, :azure
-      redirect_to @backup.dump.expiring_url(10), allow_other_host: true
+      redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true
     when :fog
       if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
-        redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true
+        redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true
       else
         redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
       end
diff --git a/app/controllers/concerns/deprecation_concern.rb b/app/controllers/concerns/deprecation_concern.rb
new file mode 100644
index 0000000000..ad8de724a1
--- /dev/null
+++ b/app/controllers/concerns/deprecation_concern.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module DeprecationConcern
+  extend ActiveSupport::Concern
+
+  class_methods do
+    def deprecate_api(date, sunset: nil, **kwargs)
+      deprecation_timestamp = "@#{date.to_datetime.to_i}"
+      sunset = sunset&.to_date&.httpdate
+
+      before_action(**kwargs) do
+        response.headers['Deprecation'] = deprecation_timestamp
+        response.headers['Sunset'] = sunset if sunset
+      end
+    end
+  end
+end
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 5f7ef8dd63..ffe612f468 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -10,8 +10,6 @@ module SignatureVerification
   EXPIRATION_WINDOW_LIMIT = 12.hours
   CLOCK_SKEW_MARGIN       = 1.hour
 
-  class SignatureVerificationError < StandardError; end
-
   def require_account_signature!
     render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
   end
@@ -34,7 +32,7 @@ module SignatureVerification
 
   def signature_key_id
     signature_params['keyId']
-  rescue SignatureVerificationError
+  rescue Mastodon::SignatureVerificationError
     nil
   end
 
@@ -45,17 +43,17 @@ module SignatureVerification
   def signed_request_actor
     return @signed_request_actor if defined?(@signed_request_actor)
 
-    raise SignatureVerificationError, 'Request not signed' unless signed_request?
-    raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
-    raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
-    raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
+    raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request?
+    raise Mastodon::SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
+    raise Mastodon::SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
+    raise Mastodon::SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
 
     verify_signature_strength!
     verify_body_digest!
 
     actor = actor_from_key_id(signature_params['keyId'])
 
-    raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
+    raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
 
     signature             = Base64.decode64(signature_params['signature'])
     compare_signed_string = build_signed_string(include_query_string: true)
@@ -68,7 +66,7 @@ module SignatureVerification
 
     actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
 
-    raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
+    raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
 
     compare_signed_string = build_signed_string(include_query_string: true)
     return actor unless verify_signature(actor, signature, compare_signed_string).nil?
@@ -78,7 +76,7 @@ module SignatureVerification
     return actor unless verify_signature(actor, signature, compare_signed_string).nil?
 
     fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
-  rescue SignatureVerificationError => e
+  rescue Mastodon::SignatureVerificationError => e
     fail_with! e.message
   rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
     fail_with! "Failed to fetch remote data: #{e.message}"
@@ -104,7 +102,7 @@ module SignatureVerification
   def signature_params
     @signature_params ||= SignatureParser.parse(request.headers['Signature'])
   rescue SignatureParser::ParsingError
-    raise SignatureVerificationError, 'Error parsing signature parameters'
+    raise Mastodon::SignatureVerificationError, 'Error parsing signature parameters'
   end
 
   def signature_algorithm
@@ -116,31 +114,31 @@ module SignatureVerification
   end
 
   def verify_signature_strength!
-    raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
-    raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
-    raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
-    raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
+    raise Mastodon::SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
+    raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
+    raise Mastodon::SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
+    raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
   end
 
   def verify_body_digest!
     return unless signed_headers.include?('digest')
-    raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
+    raise Mastodon::SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
 
     digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
     sha256  = digests.assoc('sha-256')
-    raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
+    raise Mastodon::SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
 
     return if body_digest == sha256[1]
 
     digest_size = begin
       Base64.strict_decode64(sha256[1].strip).length
     rescue ArgumentError
-      raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
+      raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
     end
 
-    raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
+    raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
 
-    raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
+    raise Mastodon::SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
   end
 
   def verify_signature(actor, signature, compare_signed_string)
@@ -165,13 +163,13 @@ module SignatureVerification
           "#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
         end
       when '(created)'
-        raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
-        raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
+        raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
+        raise Mastodon::SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
 
         "(created): #{signature_params['created']}"
       when '(expires)'
-        raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
-        raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
+        raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
+        raise Mastodon::SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
 
         "(expires): #{signature_params['expires']}"
       else
@@ -193,7 +191,7 @@ module SignatureVerification
 
       expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
     rescue ArgumentError => e
-      raise SignatureVerificationError, "Invalid Date header: #{e.message}"
+      raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}"
     end
 
     expires_time ||= created_time + 5.minutes unless created_time.nil?
@@ -233,9 +231,9 @@ module SignatureVerification
       account
     end
   rescue Mastodon::PrivateNetworkAddressError => e
-    raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
+    raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
   rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
-    raise SignatureVerificationError, e.message
+    raise Mastodon::SignatureVerificationError, e.message
   end
 
   def stoplight_wrapper
@@ -251,8 +249,8 @@ module SignatureVerification
 
     ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
   rescue Mastodon::PrivateNetworkAddressError => e
-    raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
+    raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
   rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
-    raise SignatureVerificationError, e.message
+    raise Mastodon::SignatureVerificationError, e.message
   end
 end
diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb
index 1d8ee43507..ec2256aa9c 100644
--- a/app/controllers/concerns/web_app_controller_concern.rb
+++ b/app/controllers/concerns/web_app_controller_concern.rb
@@ -46,6 +46,6 @@ module WebAppControllerConcern
   protected
 
   def set_referer_header
-    response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'origin' : 'same-origin')
+    response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'strict-origin-when-cross-origin' : 'same-origin')
   end
 end
diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb
index dd24a1b740..07677fd3f3 100644
--- a/app/controllers/disputes/base_controller.rb
+++ b/app/controllers/disputes/base_controller.rb
@@ -8,11 +8,4 @@ class Disputes::BaseController < ApplicationController
   skip_before_action :require_functional!
 
   before_action :authenticate_user!
-  before_action :set_cache_headers
-
-  private
-
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
 end
diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb
index ca5205d042..d85b017aaa 100644
--- a/app/controllers/filters/statuses_controller.rb
+++ b/app/controllers/filters/statuses_controller.rb
@@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController
   before_action :authenticate_user!
   before_action :set_filter
   before_action :set_status_filters
-  before_action :set_cache_headers
 
   PER_PAGE = 20
 
@@ -40,8 +39,4 @@ class Filters::StatusesController < ApplicationController
   def action_from_button
     'remove' if params[:remove]
   end
-
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
 end
diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb
index 6390e1ef10..20b8135908 100644
--- a/app/controllers/filters_controller.rb
+++ b/app/controllers/filters_controller.rb
@@ -5,7 +5,6 @@ class FiltersController < ApplicationController
 
   before_action :authenticate_user!
   before_action :set_filter, only: [:edit, :update, :destroy]
-  before_action :set_cache_headers
 
   def index
     @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase)
@@ -50,8 +49,4 @@ class FiltersController < ApplicationController
   def resource_params
     params.expect(custom_filter: [:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [[:id, :keyword, :whole_word, :_destroy]]])
   end
-
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
 end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index c4c52cce11..fc65333ac4 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -6,7 +6,6 @@ class InvitesController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
-  before_action :set_cache_headers
 
   def index
     authorize :invite, :create?
@@ -45,8 +44,4 @@ class InvitesController < ApplicationController
   def resource_params
     params.expect(invite: [:max_uses, :expires_in, :autofollow, :comment])
   end
-
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
 end
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 66e774425d..deafedeaef 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -5,7 +5,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
 
   before_action :store_current_location
   before_action :authenticate_resource_owner!
-  before_action :set_cache_headers
 
   content_security_policy do |p|
     p.form_action(false)
@@ -32,8 +31,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
   def truthy_param?(key)
     ActiveModel::Type::Boolean.new.cast(params[key])
   end
-
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
 end
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 9e541e5e3c..8b11a519ea 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -6,7 +6,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
   before_action :store_current_location
   before_action :authenticate_resource_owner!
   before_action :require_not_suspended!, only: :destroy
-  before_action :set_cache_headers
 
   before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
 
@@ -30,10 +29,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
     forbidden if current_account.unavailable?
   end
 
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
-
   def set_last_used_at_by_app
     @last_used_at_by_app = current_resource_owner.applications_last_used
   end
diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
index 43105d70c8..7e793fc734 100644
--- a/app/controllers/relationships_controller.rb
+++ b/app/controllers/relationships_controller.rb
@@ -6,7 +6,6 @@ class RelationshipsController < ApplicationController
   before_action :authenticate_user!
   before_action :set_accounts, only: :show
   before_action :set_relationships, only: :show
-  before_action :set_cache_headers
 
   helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
 
@@ -66,8 +65,4 @@ class RelationshipsController < ApplicationController
       'remove_domains_from_followers'
     end
   end
-
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
 end
diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb
index 9785a1b90f..8e39741f89 100644
--- a/app/controllers/settings/applications_controller.rb
+++ b/app/controllers/settings/applications_controller.rb
@@ -2,7 +2,6 @@
 
 class Settings::ApplicationsController < Settings::BaseController
   before_action :set_application, only: [:show, :update, :destroy, :regenerate]
-  before_action :prepare_scopes, only: [:create, :update]
 
   def index
     @applications = current_user.applications.order(id: :desc).page(params[:page])
@@ -60,12 +59,6 @@ class Settings::ApplicationsController < Settings::BaseController
   end
 
   def application_params
-    params
-      .expect(doorkeeper_application: [:name, :redirect_uri, :scopes, :website])
-  end
-
-  def prepare_scopes
-    scopes = application_params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
-    params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array
+    params.expect(doorkeeper_application: [:name, :redirect_uri, :website, scopes: []])
   end
 end
diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb
index 188334ac23..7f2279aa8f 100644
--- a/app/controllers/settings/base_controller.rb
+++ b/app/controllers/settings/base_controller.rb
@@ -4,14 +4,9 @@ class Settings::BaseController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
-  before_action :set_cache_headers
 
   private
 
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
-
   def require_not_suspended!
     forbidden if current_account.unavailable?
   end
diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb
index 58a4325307..7e61e6d580 100644
--- a/app/controllers/settings/pictures_controller.rb
+++ b/app/controllers/settings/pictures_controller.rb
@@ -8,7 +8,7 @@ module Settings
     def destroy
       if valid_picture?
         if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
-          ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+          ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
           redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
         else
           redirect_to settings_profile_path
diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb
index a5bb3b884f..96efa03ccf 100644
--- a/app/controllers/settings/privacy_controller.rb
+++ b/app/controllers/settings/privacy_controller.rb
@@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController
   def update
     if UpdateAccountService.new.call(@account, account_params.except(:settings))
       current_user.update!(settings_attributes: account_params[:settings])
-      ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+      ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
       redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
     else
       render :show
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index 99a647336a..04a10fbfb9 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController
 
   def update
     if UpdateAccountService.new.call(@account, account_params)
-      ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+      ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
       redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
     else
       @account.build_fields
diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb
index bed29dbeec..4b949ca72d 100644
--- a/app/controllers/settings/verifications_controller.rb
+++ b/app/controllers/settings/verifications_controller.rb
@@ -8,7 +8,7 @@ class Settings::VerificationsController < Settings::BaseController
 
   def update
     if UpdateAccountService.new.call(@account, account_params)
-      ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+      ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
       redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
     else
       render :show
diff --git a/app/controllers/severed_relationships_controller.rb b/app/controllers/severed_relationships_controller.rb
index 965753a26f..817abebf62 100644
--- a/app/controllers/severed_relationships_controller.rb
+++ b/app/controllers/severed_relationships_controller.rb
@@ -4,7 +4,6 @@ class SeveredRelationshipsController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
-  before_action :set_cache_headers
 
   before_action :set_event, only: [:following, :followers]
 
@@ -49,8 +48,4 @@ class SeveredRelationshipsController < ApplicationController
   def acct(account)
     account.local? ? account.local_username_and_domain : account.acct
   end
-
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
 end
diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb
index 583254ec27..a25e544392 100644
--- a/app/controllers/statuses_cleanup_controller.rb
+++ b/app/controllers/statuses_cleanup_controller.rb
@@ -5,7 +5,6 @@ class StatusesCleanupController < ApplicationController
 
   before_action :authenticate_user!
   before_action :set_policy
-  before_action :set_cache_headers
 
   def show; end
 
@@ -30,8 +29,4 @@ class StatusesCleanupController < ApplicationController
   def resource_params
     params.expect(account_statuses_cleanup_policy: [:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis])
   end
-
-  def set_cache_headers
-    response.cache_control.replace(private: true, no_store: true)
-  end
 end
diff --git a/app/controllers/system_css_controller.rb b/app/controllers/system_css_controller.rb
index a19728bbfd..dd90491894 100644
--- a/app/controllers/system_css_controller.rb
+++ b/app/controllers/system_css_controller.rb
@@ -1,16 +1,8 @@
 # frozen_string_literal: true
 
 class SystemCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
-  before_action :set_user_roles
-
   def show
     expires_in 3.minutes, public: true
     render content_type: 'text/css'
   end
-
-  private
-
-  def set_user_roles
-    @user_roles = UserRole.providing_styles
-  end
 end
diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb
index c7a59660cf..33da1f7216 100644
--- a/app/helpers/admin/trends/statuses_helper.rb
+++ b/app/helpers/admin/trends/statuses_helper.rb
@@ -2,11 +2,18 @@
 
 module Admin::Trends::StatusesHelper
   def one_line_preview(status)
-    text = if status.local?
-             status.text.split("\n").first
-           else
-             Nokogiri::HTML5(status.text).css('html > body > *').first&.text
-           end
+    text = begin
+      if status.local?
+        status.text.split("\n").first
+      else
+        Nokogiri::HTML5(status.text).css('html > body > *').first&.text
+      end
+    rescue ArgumentError
+      # This can happen if one of the Nokogumbo limits is encountered
+      # Unfortunately, it does not use a more precise error class
+      # nor allows more graceful handling
+      ''
+    end
 
     return '' if text.blank?
 
diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/json_ld_helper.rb
index 2a5c2d8826..693cdf730f 100644
--- a/app/helpers/json_ld_helper.rb
+++ b/app/helpers/json_ld_helper.rb
@@ -163,24 +163,49 @@ module JsonLdHelper
     end
   end
 
-  def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
+  # Fetch the resource given by uri.
+  # @param uri [String]
+  # @param id_is_known [Boolean]
+  # @param on_behalf_of [nil, Account]
+  # @param raise_on_error [Symbol<:all, :temporary, :none>] See {#fetch_resource_without_id_validation} for possible values
+  def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: :none, request_options: {})
     unless id_is_known
-      json = fetch_resource_without_id_validation(uri, on_behalf_of)
+      json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error)
 
       return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
 
       uri = json['id']
     end
 
-    json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
+    json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error, request_options: request_options)
     json.present? && json['id'] == uri ? json : nil
   end
 
-  def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
+  # Fetch the resource given by uri
+  #
+  # If an error is raised, it contains the response and can be captured for handling like
+  #
+  #     begin
+  #       fetch_resource_without_id_validation(uri, nil, true)
+  #     rescue Mastodon::UnexpectedResponseError => e
+  #       e.response
+  #     end
+  #
+  # @param uri [String]
+  # @param on_behalf_of [nil, Account]
+  # @param raise_on_error [Symbol<:all, :temporary, :none>]
+  #   - +:all+ - raise if response code is not in the 2xx range
+  #   - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404
+  #     (see {#response_error_unsalvageable} )
+  #   - +:none+ - do not raise, return +nil+
+  def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: :none, request_options: {})
     on_behalf_of ||= Account.representative
 
     build_request(uri, on_behalf_of, options: request_options).perform do |response|
-      raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
+      raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && (
+        raise_on_error == :all ||
+        (!response_error_unsalvageable?(response) && raise_on_error == :temporary)
+      )
 
       body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
     end
diff --git a/app/inputs/date_of_birth_input.rb b/app/inputs/date_of_birth_input.rb
new file mode 100644
index 0000000000..131234b02e
--- /dev/null
+++ b/app/inputs/date_of_birth_input.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class DateOfBirthInput < SimpleForm::Inputs::Base
+  OPTIONS = [
+    { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze,
+    { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze,
+    { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze,
+  ].freeze
+
+  def input(wrapper_options = nil)
+    merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
+    merged_input_options[:inputmode] = 'numeric'
+
+    values = (object.public_send(attribute_name) || '').split('.')
+
+    safe_join(Array.new(3) do |index|
+      options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index]
+      @builder.text_field("#{attribute_name}(#{index + 1}i)", options)
+    end)
+  end
+
+  def label_target
+    "#{attribute_name}_1i"
+  end
+
+  private
+
+  def generate_id(index)
+    "#{object_name}_#{attribute_name}_#{index + 1}i"
+  end
+end
diff --git a/app/javascript/entrypoints/embed.tsx b/app/javascript/entrypoints/embed.tsx
index cb62727563..6c091e4d07 100644
--- a/app/javascript/entrypoints/embed.tsx
+++ b/app/javascript/entrypoints/embed.tsx
@@ -1,7 +1,7 @@
 import './public-path';
 import { createRoot } from 'react-dom/client';
 
-import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
+import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
 
 import { start } from '../mastodon/common';
 import { Status } from '../mastodon/features/standalone/status';
diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx
index 0560e76628..9374d6b2d1 100644
--- a/app/javascript/entrypoints/public.tsx
+++ b/app/javascript/entrypoints/public.tsx
@@ -68,7 +68,7 @@ function loaded() {
 
     if (id) message = localeData[id];
 
-    if (!message) message = defaultMessage as string;
+    message ??= defaultMessage as string;
 
     const messageFormat = new IntlMessageFormat(message, locale);
     return messageFormat.format(values) as string;
diff --git a/app/javascript/icons/android-chrome-144x144.png b/app/javascript/icons/android-chrome-144x144.png
old mode 100755
new mode 100644
index d636e94c43..698fb4a260
Binary files a/app/javascript/icons/android-chrome-144x144.png and b/app/javascript/icons/android-chrome-144x144.png differ
diff --git a/app/javascript/icons/android-chrome-192x192.png b/app/javascript/icons/android-chrome-192x192.png
old mode 100755
new mode 100644
index 4a2681ffb9..2b6b632648
Binary files a/app/javascript/icons/android-chrome-192x192.png and b/app/javascript/icons/android-chrome-192x192.png differ
diff --git a/app/javascript/icons/android-chrome-256x256.png b/app/javascript/icons/android-chrome-256x256.png
old mode 100755
new mode 100644
index 8fab493ede..51e3849a26
Binary files a/app/javascript/icons/android-chrome-256x256.png and b/app/javascript/icons/android-chrome-256x256.png differ
diff --git a/app/javascript/icons/android-chrome-36x36.png b/app/javascript/icons/android-chrome-36x36.png
old mode 100755
new mode 100644
index 335d012db1..925f69c4fc
Binary files a/app/javascript/icons/android-chrome-36x36.png and b/app/javascript/icons/android-chrome-36x36.png differ
diff --git a/app/javascript/icons/android-chrome-384x384.png b/app/javascript/icons/android-chrome-384x384.png
old mode 100755
new mode 100644
index 02b1e6fced..9d256a83cb
Binary files a/app/javascript/icons/android-chrome-384x384.png and b/app/javascript/icons/android-chrome-384x384.png differ
diff --git a/app/javascript/icons/android-chrome-48x48.png b/app/javascript/icons/android-chrome-48x48.png
old mode 100755
new mode 100644
index 43cf411b8c..bcfe7475d0
Binary files a/app/javascript/icons/android-chrome-48x48.png and b/app/javascript/icons/android-chrome-48x48.png differ
diff --git a/app/javascript/icons/android-chrome-512x512.png b/app/javascript/icons/android-chrome-512x512.png
old mode 100755
new mode 100644
index 1856b80c7c..bffacfb699
Binary files a/app/javascript/icons/android-chrome-512x512.png and b/app/javascript/icons/android-chrome-512x512.png differ
diff --git a/app/javascript/icons/android-chrome-72x72.png b/app/javascript/icons/android-chrome-72x72.png
old mode 100755
new mode 100644
index 335008bf85..16679d5731
Binary files a/app/javascript/icons/android-chrome-72x72.png and b/app/javascript/icons/android-chrome-72x72.png differ
diff --git a/app/javascript/icons/android-chrome-96x96.png b/app/javascript/icons/android-chrome-96x96.png
old mode 100755
new mode 100644
index d1cb095822..9ade87cf32
Binary files a/app/javascript/icons/android-chrome-96x96.png and b/app/javascript/icons/android-chrome-96x96.png differ
diff --git a/app/javascript/icons/apple-touch-icon-1024x1024.png b/app/javascript/icons/apple-touch-icon-1024x1024.png
old mode 100755
new mode 100644
index c2a2d516ef..8ec371eb27
Binary files a/app/javascript/icons/apple-touch-icon-1024x1024.png and b/app/javascript/icons/apple-touch-icon-1024x1024.png differ
diff --git a/app/javascript/icons/apple-touch-icon-114x114.png b/app/javascript/icons/apple-touch-icon-114x114.png
old mode 100755
new mode 100644
index 218b415439..e1563f51e5
Binary files a/app/javascript/icons/apple-touch-icon-114x114.png and b/app/javascript/icons/apple-touch-icon-114x114.png differ
diff --git a/app/javascript/icons/apple-touch-icon-120x120.png b/app/javascript/icons/apple-touch-icon-120x120.png
old mode 100755
new mode 100644
index be53bc7c10..e9a5f5b0e5
Binary files a/app/javascript/icons/apple-touch-icon-120x120.png and b/app/javascript/icons/apple-touch-icon-120x120.png differ
diff --git a/app/javascript/icons/apple-touch-icon-144x144.png b/app/javascript/icons/apple-touch-icon-144x144.png
old mode 100755
new mode 100644
index cbb055732f..698fb4a260
Binary files a/app/javascript/icons/apple-touch-icon-144x144.png and b/app/javascript/icons/apple-touch-icon-144x144.png differ
diff --git a/app/javascript/icons/apple-touch-icon-152x152.png b/app/javascript/icons/apple-touch-icon-152x152.png
old mode 100755
new mode 100644
index 3a7975c054..0cc93cc288
Binary files a/app/javascript/icons/apple-touch-icon-152x152.png and b/app/javascript/icons/apple-touch-icon-152x152.png differ
diff --git a/app/javascript/icons/apple-touch-icon-167x167.png b/app/javascript/icons/apple-touch-icon-167x167.png
old mode 100755
new mode 100644
index 25be4eb5f5..9bbbf53120
Binary files a/app/javascript/icons/apple-touch-icon-167x167.png and b/app/javascript/icons/apple-touch-icon-167x167.png differ
diff --git a/app/javascript/icons/apple-touch-icon-180x180.png b/app/javascript/icons/apple-touch-icon-180x180.png
old mode 100755
new mode 100644
index dc0e9bc20b..329b803b91
Binary files a/app/javascript/icons/apple-touch-icon-180x180.png and b/app/javascript/icons/apple-touch-icon-180x180.png differ
diff --git a/app/javascript/icons/apple-touch-icon-192x192.png b/app/javascript/icons/apple-touch-icon-192x192.png
new file mode 100644
index 0000000000..2b6b632648
Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-192x192.png differ
diff --git a/app/javascript/icons/apple-touch-icon-256x256.png b/app/javascript/icons/apple-touch-icon-256x256.png
new file mode 100644
index 0000000000..51e3849a26
Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-256x256.png differ
diff --git a/app/javascript/icons/apple-touch-icon-36x36.png b/app/javascript/icons/apple-touch-icon-36x36.png
new file mode 100644
index 0000000000..925f69c4fc
Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-36x36.png differ
diff --git a/app/javascript/icons/apple-touch-icon-384x384.png b/app/javascript/icons/apple-touch-icon-384x384.png
new file mode 100644
index 0000000000..9d256a83cb
Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-384x384.png differ
diff --git a/app/javascript/icons/apple-touch-icon-48x48.png b/app/javascript/icons/apple-touch-icon-48x48.png
new file mode 100644
index 0000000000..bcfe7475d0
Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-48x48.png differ
diff --git a/app/javascript/icons/apple-touch-icon-512x512.png b/app/javascript/icons/apple-touch-icon-512x512.png
new file mode 100644
index 0000000000..bffacfb699
Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-512x512.png differ
diff --git a/app/javascript/icons/apple-touch-icon-57x57.png b/app/javascript/icons/apple-touch-icon-57x57.png
old mode 100755
new mode 100644
index bb0dc957cd..e00e142c64
Binary files a/app/javascript/icons/apple-touch-icon-57x57.png and b/app/javascript/icons/apple-touch-icon-57x57.png differ
diff --git a/app/javascript/icons/apple-touch-icon-60x60.png b/app/javascript/icons/apple-touch-icon-60x60.png
old mode 100755
new mode 100644
index 9143a0bf07..011285b564
Binary files a/app/javascript/icons/apple-touch-icon-60x60.png and b/app/javascript/icons/apple-touch-icon-60x60.png differ
diff --git a/app/javascript/icons/apple-touch-icon-72x72.png b/app/javascript/icons/apple-touch-icon-72x72.png
old mode 100755
new mode 100644
index 2b7d19484c..16679d5731
Binary files a/app/javascript/icons/apple-touch-icon-72x72.png and b/app/javascript/icons/apple-touch-icon-72x72.png differ
diff --git a/app/javascript/icons/apple-touch-icon-76x76.png b/app/javascript/icons/apple-touch-icon-76x76.png
old mode 100755
new mode 100644
index 0985e33bcb..83c8748876
Binary files a/app/javascript/icons/apple-touch-icon-76x76.png and b/app/javascript/icons/apple-touch-icon-76x76.png differ
diff --git a/app/javascript/icons/apple-touch-icon-96x96.png b/app/javascript/icons/apple-touch-icon-96x96.png
new file mode 100644
index 0000000000..9ade87cf32
Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-96x96.png differ
diff --git a/app/javascript/icons/favicon-16x16.png b/app/javascript/icons/favicon-16x16.png
old mode 100755
new mode 100644
index 1326ba0462..7f865cfe96
Binary files a/app/javascript/icons/favicon-16x16.png and b/app/javascript/icons/favicon-16x16.png differ
diff --git a/app/javascript/icons/favicon-32x32.png b/app/javascript/icons/favicon-32x32.png
old mode 100755
new mode 100644
index f5058cb0a5..7f865cfe96
Binary files a/app/javascript/icons/favicon-32x32.png and b/app/javascript/icons/favicon-32x32.png differ
diff --git a/app/javascript/icons/favicon-48x48.png b/app/javascript/icons/favicon-48x48.png
old mode 100755
new mode 100644
index 6253d054c7..7f865cfe96
Binary files a/app/javascript/icons/favicon-48x48.png and b/app/javascript/icons/favicon-48x48.png differ
diff --git a/app/javascript/images/archetypes/booster.png b/app/javascript/images/archetypes/booster.png
index 18c92dfb7d..df2a0226f8 100755
Binary files a/app/javascript/images/archetypes/booster.png and b/app/javascript/images/archetypes/booster.png differ
diff --git a/app/javascript/images/archetypes/lurker.png b/app/javascript/images/archetypes/lurker.png
index 8e1d6451b0..e37f98aab2 100755
Binary files a/app/javascript/images/archetypes/lurker.png and b/app/javascript/images/archetypes/lurker.png differ
diff --git a/app/javascript/images/archetypes/oracle.png b/app/javascript/images/archetypes/oracle.png
index 2afd3c72e1..9d4e2177c5 100755
Binary files a/app/javascript/images/archetypes/oracle.png and b/app/javascript/images/archetypes/oracle.png differ
diff --git a/app/javascript/images/archetypes/pollster.png b/app/javascript/images/archetypes/pollster.png
index b838fccdd6..9fe6281af0 100755
Binary files a/app/javascript/images/archetypes/pollster.png and b/app/javascript/images/archetypes/pollster.png differ
diff --git a/app/javascript/images/archetypes/replier.png b/app/javascript/images/archetypes/replier.png
index b298d4221c..6c6325b9f1 100755
Binary files a/app/javascript/images/archetypes/replier.png and b/app/javascript/images/archetypes/replier.png differ
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index 3d0e8b8c90..d821381ce0 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -142,6 +142,13 @@ export function fetchAccountFail(id, error) {
   };
 }
 
+/**
+ * @param {string} id
+ * @param {Object} options
+ * @param {boolean} [options.reblogs]
+ * @param {boolean} [options.notify]
+ * @returns {function(): void}
+ */
 export function followAccount(id, options = { reblogs: true }) {
   return (dispatch, getState) => {
     const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
diff --git a/app/javascript/mastodon/actions/alerts.ts b/app/javascript/mastodon/actions/alerts.ts
index a521f3ef35..4fd293e252 100644
--- a/app/javascript/mastodon/actions/alerts.ts
+++ b/app/javascript/mastodon/actions/alerts.ts
@@ -1,14 +1,11 @@
 import { defineMessages } from 'react-intl';
-import type { MessageDescriptor } from 'react-intl';
+
+import { createAction } from '@reduxjs/toolkit';
 
 import { AxiosError } from 'axios';
 import type { AxiosResponse } from 'axios';
 
-interface Alert {
-  title: string | MessageDescriptor;
-  message: string | MessageDescriptor;
-  values?: Record<string, string | number | Date>;
-}
+import type { Alert } from 'mastodon/models/alert';
 
 interface ApiErrorResponse {
   error?: string;
@@ -30,24 +27,13 @@ const messages = defineMessages({
   },
 });
 
-export const ALERT_SHOW = 'ALERT_SHOW';
-export const ALERT_DISMISS = 'ALERT_DISMISS';
-export const ALERT_CLEAR = 'ALERT_CLEAR';
-export const ALERT_NOOP = 'ALERT_NOOP';
+export const dismissAlert = createAction<{ key: number }>('alerts/dismiss');
 
-export const dismissAlert = (alert: Alert) => ({
-  type: ALERT_DISMISS,
-  alert,
-});
+export const clearAlerts = createAction('alerts/clear');
 
-export const clearAlert = () => ({
-  type: ALERT_CLEAR,
-});
+export const showAlert = createAction<Omit<Alert, 'key'>>('alerts/show');
 
-export const showAlert = (alert: Alert) => ({
-  type: ALERT_SHOW,
-  alert,
-});
+const ignoreAlert = createAction('alerts/ignore');
 
 export const showAlertForError = (error: unknown, skipNotFound = false) => {
   if (error instanceof AxiosError && error.response) {
@@ -56,7 +42,7 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => {
 
     // Skip these errors as they are reflected in the UI
     if (skipNotFound && (status === 404 || status === 410)) {
-      return { type: ALERT_NOOP };
+      return ignoreAlert();
     }
 
     // Rate limit errors
@@ -76,9 +62,9 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => {
     });
   }
 
-  // An aborted request, e.g. due to reloading the browser window, it not really error
+  // An aborted request, e.g. due to reloading the browser window, is not really an error
   if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) {
-    return { type: ALERT_NOOP };
+    return ignoreAlert();
   }
 
   console.error(error);
diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js
index 727f800af3..279ec1bef7 100644
--- a/app/javascript/mastodon/actions/domain_blocks.js
+++ b/app/javascript/mastodon/actions/domain_blocks.js
@@ -12,14 +12,6 @@ export const DOMAIN_BLOCK_FAIL    = 'DOMAIN_BLOCK_FAIL';
 export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
 export const DOMAIN_UNBLOCK_FAIL    = 'DOMAIN_UNBLOCK_FAIL';
 
-export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
-export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS';
-export const DOMAIN_BLOCKS_FETCH_FAIL    = 'DOMAIN_BLOCKS_FETCH_FAIL';
-
-export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST';
-export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS';
-export const DOMAIN_BLOCKS_EXPAND_FAIL    = 'DOMAIN_BLOCKS_EXPAND_FAIL';
-
 export function blockDomain(domain) {
   return (dispatch, getState) => {
     dispatch(blockDomainRequest(domain));
@@ -79,80 +71,6 @@ export function unblockDomainFail(domain, error) {
   };
 }
 
-export function fetchDomainBlocks() {
-  return (dispatch) => {
-    dispatch(fetchDomainBlocksRequest());
-
-    api().get('/api/v1/domain_blocks').then(response => {
-      const next = getLinks(response).refs.find(link => link.rel === 'next');
-      dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
-    }).catch(err => {
-      dispatch(fetchDomainBlocksFail(err));
-    });
-  };
-}
-
-export function fetchDomainBlocksRequest() {
-  return {
-    type: DOMAIN_BLOCKS_FETCH_REQUEST,
-  };
-}
-
-export function fetchDomainBlocksSuccess(domains, next) {
-  return {
-    type: DOMAIN_BLOCKS_FETCH_SUCCESS,
-    domains,
-    next,
-  };
-}
-
-export function fetchDomainBlocksFail(error) {
-  return {
-    type: DOMAIN_BLOCKS_FETCH_FAIL,
-    error,
-  };
-}
-
-export function expandDomainBlocks() {
-  return (dispatch, getState) => {
-    const url = getState().getIn(['domain_lists', 'blocks', 'next']);
-
-    if (!url) {
-      return;
-    }
-
-    dispatch(expandDomainBlocksRequest());
-
-    api().get(url).then(response => {
-      const next = getLinks(response).refs.find(link => link.rel === 'next');
-      dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
-    }).catch(err => {
-      dispatch(expandDomainBlocksFail(err));
-    });
-  };
-}
-
-export function expandDomainBlocksRequest() {
-  return {
-    type: DOMAIN_BLOCKS_EXPAND_REQUEST,
-  };
-}
-
-export function expandDomainBlocksSuccess(domains, next) {
-  return {
-    type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
-    domains,
-    next,
-  };
-}
-
-export function expandDomainBlocksFail(error) {
-  return {
-    type: DOMAIN_BLOCKS_EXPAND_FAIL,
-    error,
-  };
-}
-
 export const initDomainBlockModal = account => dispatch => dispatch(openModal({
   modalType: 'DOMAIN_BLOCK',
   modalProps: {
diff --git a/app/javascript/mastodon/actions/dropdown_menu.ts b/app/javascript/mastodon/actions/dropdown_menu.ts
index 3694df1ae0..d9d395ba33 100644
--- a/app/javascript/mastodon/actions/dropdown_menu.ts
+++ b/app/javascript/mastodon/actions/dropdown_menu.ts
@@ -1,11 +1,11 @@
 import { createAction } from '@reduxjs/toolkit';
 
 export const openDropdownMenu = createAction<{
-  id: string;
+  id: number;
   keyboard: boolean;
-  scrollKey: string;
+  scrollKey?: string;
 }>('dropdownMenu/open');
 
-export const closeDropdownMenu = createAction<{ id: string }>(
+export const closeDropdownMenu = createAction<{ id: number }>(
   'dropdownMenu/close',
 );
diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js
index 380190a910..fc165b1a1f 100644
--- a/app/javascript/mastodon/actions/importer/index.js
+++ b/app/javascript/mastodon/actions/importer/index.js
@@ -75,7 +75,7 @@ export function importFetchedStatuses(statuses) {
       }
 
       if (status.poll?.id) {
-        pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls.get(status.poll.id)));
+        pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id]));
       }
 
       if (status.card) {
diff --git a/app/javascript/mastodon/actions/polls.ts b/app/javascript/mastodon/actions/polls.ts
index 28f729394b..65a96e8f62 100644
--- a/app/javascript/mastodon/actions/polls.ts
+++ b/app/javascript/mastodon/actions/polls.ts
@@ -15,7 +15,7 @@ export const importFetchedPoll = createAppAsyncThunk(
 
     dispatch(
       importPolls({
-        polls: [createPollFromServerJSON(poll, getState().polls.get(poll.id))],
+        polls: [createPollFromServerJSON(poll, getState().polls[poll.id])],
       }),
     );
   },
diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js
index fbd89f9d4b..7659fb5f98 100644
--- a/app/javascript/mastodon/actions/settings.js
+++ b/app/javascript/mastodon/actions/settings.js
@@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => {
   api().put('/api/web/settings', { data })
     .then(() => dispatch({ type: SETTING_SAVE }))
     .catch(error => dispatch(showAlertForError(error)));
-}, 5000, { trailing: true });
+}, 2000, { leading: true, trailing: true });
 
 export function saveSettings() {
   return (dispatch, getState) => debouncedSave(dispatch, getState);
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 40ead34782..5064e65e7b 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -148,7 +148,7 @@ export function deleteStatus(id, withRedraft = false) {
 
     dispatch(deleteStatusRequest(id));
 
-    api().delete(`/api/v1/statuses/${id}`).then(response => {
+    api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
       dispatch(deleteStatusSuccess(id));
       dispatch(deleteFromTimelines(id));
       dispatch(importFetchedAccount(response.data.account));
diff --git a/app/javascript/mastodon/actions/tags.js b/app/javascript/mastodon/actions/tags.js
deleted file mode 100644
index 6e0c95288a..0000000000
--- a/app/javascript/mastodon/actions/tags.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import api, { getLinks } from '../api';
-
-export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
-export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
-export const FOLLOWED_HASHTAGS_FETCH_FAIL    = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
-
-export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
-export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
-export const FOLLOWED_HASHTAGS_EXPAND_FAIL    = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
-
-export const fetchFollowedHashtags = () => (dispatch) => {
-  dispatch(fetchFollowedHashtagsRequest());
-
-  api().get('/api/v1/followed_tags').then(response => {
-    const next = getLinks(response).refs.find(link => link.rel === 'next');
-    dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
-  }).catch(err => {
-    dispatch(fetchFollowedHashtagsFail(err));
-  });
-};
-
-export function fetchFollowedHashtagsRequest() {
-  return {
-    type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
-  };
-}
-
-export function fetchFollowedHashtagsSuccess(followed_tags, next) {
-  return {
-    type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
-    followed_tags,
-    next,
-  };
-}
-
-export function fetchFollowedHashtagsFail(error) {
-  return {
-    type: FOLLOWED_HASHTAGS_FETCH_FAIL,
-    error,
-  };
-}
-
-export function expandFollowedHashtags() {
-  return (dispatch, getState) => {
-    const url = getState().getIn(['followed_tags', 'next']);
-
-    if (url === null) {
-      return;
-    }
-
-    dispatch(expandFollowedHashtagsRequest());
-
-    api().get(url).then(response => {
-      const next = getLinks(response).refs.find(link => link.rel === 'next');
-      dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
-    }).catch(error => {
-      dispatch(expandFollowedHashtagsFail(error));
-    });
-  };
-}
-
-export function expandFollowedHashtagsRequest() {
-  return {
-    type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
-  };
-}
-
-export function expandFollowedHashtagsSuccess(followed_tags, next) {
-  return {
-    type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
-    followed_tags,
-    next,
-  };
-}
-
-export function expandFollowedHashtagsFail(error) {
-  return {
-    type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
-    error,
-  };
-}
diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts
index f0663ded40..a41b058d2c 100644
--- a/app/javascript/mastodon/api.ts
+++ b/app/javascript/mastodon/api.ts
@@ -1,4 +1,9 @@
-import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios';
+import type {
+  AxiosError,
+  AxiosResponse,
+  Method,
+  RawAxiosRequestHeaders,
+} from 'axios';
 import axios from 'axios';
 import LinkHeader from 'http-link-header';
 
@@ -41,7 +46,7 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
 
 // eslint-disable-next-line import/no-default-export
 export default function api(withAuthorization = true) {
-  return axios.create({
+  const instance = axios.create({
     transitional: {
       clarifyTimeoutError: true,
     },
@@ -60,6 +65,22 @@ export default function api(withAuthorization = true) {
       },
     ],
   });
+
+  instance.interceptors.response.use(
+    (response: AxiosResponse) => {
+      if (response.headers.deprecation) {
+        console.warn(
+          `Deprecated request: ${response.config.method} ${response.config.url}`,
+        );
+      }
+      return response;
+    },
+    (error: AxiosError) => {
+      return Promise.reject(error);
+    },
+  );
+
+  return instance;
 }
 
 type RequestParamsOrData = Record<string, unknown>;
diff --git a/app/javascript/mastodon/api/domain_blocks.ts b/app/javascript/mastodon/api/domain_blocks.ts
new file mode 100644
index 0000000000..4e153b0ee9
--- /dev/null
+++ b/app/javascript/mastodon/api/domain_blocks.ts
@@ -0,0 +1,13 @@
+import api, { getLinks } from 'mastodon/api';
+
+export const apiGetDomainBlocks = async (url?: string) => {
+  const response = await api().request<string[]>({
+    method: 'GET',
+    url: url ?? '/api/v1/domain_blocks',
+  });
+
+  return {
+    domains: response.data,
+    links: getLinks(response),
+  };
+};
diff --git a/app/javascript/mastodon/api/instance.ts b/app/javascript/mastodon/api/instance.ts
index ec9146fb34..764e8daab2 100644
--- a/app/javascript/mastodon/api/instance.ts
+++ b/app/javascript/mastodon/api/instance.ts
@@ -4,8 +4,12 @@ import type {
   ApiPrivacyPolicyJSON,
 } from 'mastodon/api_types/instance';
 
-export const apiGetTermsOfService = () =>
-  apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
+export const apiGetTermsOfService = (version?: string) =>
+  apiRequestGet<ApiTermsOfServiceJSON>(
+    version
+      ? `v1/instance/terms_of_service/${version}`
+      : 'v1/instance/terms_of_service',
+  );
 
 export const apiGetPrivacyPolicy = () =>
   apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
diff --git a/app/javascript/mastodon/api/tags.ts b/app/javascript/mastodon/api/tags.ts
index 2cb802800c..4b111def81 100644
--- a/app/javascript/mastodon/api/tags.ts
+++ b/app/javascript/mastodon/api/tags.ts
@@ -1,4 +1,4 @@
-import { apiRequestPost, apiRequestGet } from 'mastodon/api';
+import api, { getLinks, apiRequestPost, apiRequestGet } from 'mastodon/api';
 import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
 
 export const apiGetTag = (tagId: string) =>
@@ -9,3 +9,15 @@ export const apiFollowTag = (tagId: string) =>
 
 export const apiUnfollowTag = (tagId: string) =>
   apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`);
+
+export const apiGetFollowedTags = async (url?: string) => {
+  const response = await api().request<ApiHashtagJSON[]>({
+    method: 'GET',
+    url: url ?? '/api/v1/followed_tags',
+  });
+
+  return {
+    tags: response.data,
+    links: getLinks(response),
+  };
+};
diff --git a/app/javascript/mastodon/api_types/instance.ts b/app/javascript/mastodon/api_types/instance.ts
index ead9774515..3a29684b70 100644
--- a/app/javascript/mastodon/api_types/instance.ts
+++ b/app/javascript/mastodon/api_types/instance.ts
@@ -1,5 +1,7 @@
 export interface ApiTermsOfServiceJSON {
-  updated_at: string;
+  effective_date: string;
+  effective: boolean;
+  succeeded_by: string | null;
   content: string;
 }
 
diff --git a/app/javascript/mastodon/api_types/polls.ts b/app/javascript/mastodon/api_types/polls.ts
index 275ca29fd7..891a2faba7 100644
--- a/app/javascript/mastodon/api_types/polls.ts
+++ b/app/javascript/mastodon/api_types/polls.ts
@@ -13,7 +13,7 @@ export interface ApiPollJSON {
   expired: boolean;
   multiple: boolean;
   votes_count: number;
-  voters_count: number;
+  voters_count: number | null;
 
   options: ApiPollOptionJSON[];
   emojis: ApiCustomEmojiJSON[];
diff --git a/app/javascript/mastodon/components/account.tsx b/app/javascript/mastodon/components/account.tsx
index f5b28ecaaa..c6c2204085 100644
--- a/app/javascript/mastodon/components/account.tsx
+++ b/app/javascript/mastodon/components/account.tsx
@@ -1,5 +1,6 @@
 import type { ReactNode } from 'react';
-import React, { useCallback } from 'react';
+import type React from 'react';
+import { useCallback, useMemo } from 'react';
 
 import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
 
@@ -13,18 +14,19 @@ import {
   muteAccount,
   unmuteAccount,
 } from 'mastodon/actions/accounts';
+import { openModal } from 'mastodon/actions/modal';
 import { initMuteModal } from 'mastodon/actions/mutes';
 import { Avatar } from 'mastodon/components/avatar';
 import { Button } from 'mastodon/components/button';
 import { FollowersCounter } from 'mastodon/components/counters';
 import { DisplayName } from 'mastodon/components/display_name';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { FollowButton } from 'mastodon/components/follow_button';
 import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
 import { ShortNumber } from 'mastodon/components/short_number';
 import { Skeleton } from 'mastodon/components/skeleton';
 import { VerifiedBadge } from 'mastodon/components/verified_badge';
-import DropdownMenu from 'mastodon/containers/dropdown_menu_container';
-import { me } from 'mastodon/initial_state';
+import type { MenuItem } from 'mastodon/models/dropdown_menu';
 import { useAppSelector, useAppDispatch } from 'mastodon/store';
 
 const messages = defineMessages({
@@ -47,6 +49,14 @@ const messages = defineMessages({
   mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
   block: { id: 'account.block_short', defaultMessage: 'Block' },
   more: { id: 'status.more', defaultMessage: 'More' },
+  addToLists: {
+    id: 'account.add_or_remove_from_list',
+    defaultMessage: 'Add or Remove from lists',
+  },
+  openOriginalPage: {
+    id: 'account.open_original_page',
+    defaultMessage: 'Open original page',
+  },
 });
 
 export const Account: React.FC<{
@@ -72,6 +82,7 @@ export const Account: React.FC<{
   const account = useAppSelector((state) => state.accounts.get(id));
   const relationship = useAppSelector((state) => state.relationships.get(id));
   const dispatch = useAppDispatch();
+  const accountUrl = account?.url;
 
   const handleBlock = useCallback(() => {
     if (relationship?.blocking) {
@@ -89,13 +100,62 @@ export const Account: React.FC<{
     }
   }, [dispatch, id, account, relationship]);
 
-  const handleMuteNotifications = useCallback(() => {
-    dispatch(muteAccount(id, true));
-  }, [dispatch, id]);
+  const menu = useMemo(() => {
+    let arr: MenuItem[] = [];
 
-  const handleUnmuteNotifications = useCallback(() => {
-    dispatch(muteAccount(id, false));
-  }, [dispatch, id]);
+    if (defaultAction === 'mute') {
+      const handleMuteNotifications = () => {
+        dispatch(muteAccount(id, true));
+      };
+
+      const handleUnmuteNotifications = () => {
+        dispatch(muteAccount(id, false));
+      };
+
+      arr = [
+        {
+          text: intl.formatMessage(
+            relationship?.muting_notifications
+              ? messages.unmute_notifications
+              : messages.mute_notifications,
+          ),
+          action: relationship?.muting_notifications
+            ? handleUnmuteNotifications
+            : handleMuteNotifications,
+        },
+      ];
+    } else if (defaultAction !== 'block') {
+      const handleAddToLists = () => {
+        dispatch(
+          openModal({
+            modalType: 'LIST_ADDER',
+            modalProps: {
+              accountId: id,
+            },
+          }),
+        );
+      };
+
+      arr = [
+        {
+          text: intl.formatMessage(messages.addToLists),
+          action: handleAddToLists,
+        },
+      ];
+
+      if (accountUrl) {
+        arr.unshift(
+          {
+            text: intl.formatMessage(messages.openOriginalPage),
+            href: accountUrl,
+          },
+          null,
+        );
+      }
+    }
+
+    return arr;
+  }, [dispatch, intl, id, accountUrl, relationship, defaultAction]);
 
   if (hidden) {
     return (
@@ -106,73 +166,46 @@ export const Account: React.FC<{
     );
   }
 
-  let buttons;
+  let button: React.ReactNode, dropdown: React.ReactNode;
 
-  if (account && account.id !== me && relationship) {
-    const { requested, blocking, muting } = relationship;
+  if (menu.length > 0) {
+    dropdown = (
+      <Dropdown
+        items={menu}
+        icon='ellipsis-h'
+        iconComponent={MoreHorizIcon}
+        title={intl.formatMessage(messages.more)}
+      />
+    );
+  }
 
-    if (requested) {
-      buttons = <FollowButton accountId={id} />;
-    } else if (blocking) {
-      buttons = (
-        <Button
-          text={intl.formatMessage(messages.unblock)}
-          onClick={handleBlock}
-        />
-      );
-    } else if (muting) {
-      const menu = [
-        {
-          text: intl.formatMessage(
-            relationship.muting_notifications
-              ? messages.unmute_notifications
-              : messages.mute_notifications,
-          ),
-          action: relationship.muting_notifications
-            ? handleUnmuteNotifications
-            : handleMuteNotifications,
-        },
-      ];
-
-      buttons = (
-        <>
-          <DropdownMenu
-            items={menu}
-            icon='ellipsis-h'
-            iconComponent={MoreHorizIcon}
-            direction='right'
-            title={intl.formatMessage(messages.more)}
-          />
-
-          <Button
-            text={intl.formatMessage(messages.unmute)}
-            onClick={handleMute}
-          />
-        </>
-      );
-    } else if (defaultAction === 'mute') {
-      buttons = (
-        <Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />
-      );
-    } else if (defaultAction === 'block') {
-      buttons = (
-        <Button
-          text={intl.formatMessage(messages.block)}
-          onClick={handleBlock}
-        />
-      );
-    } else {
-      buttons = <FollowButton accountId={id} />;
-    }
+  if (defaultAction === 'block') {
+    button = (
+      <Button
+        text={intl.formatMessage(
+          relationship?.blocking ? messages.unblock : messages.block,
+        )}
+        onClick={handleBlock}
+      />
+    );
+  } else if (defaultAction === 'mute') {
+    button = (
+      <Button
+        text={intl.formatMessage(
+          relationship?.muting ? messages.unmute : messages.mute,
+        )}
+        onClick={handleMute}
+      />
+    );
   } else {
-    buttons = <FollowButton accountId={id} />;
+    button = <FollowButton accountId={id} />;
   }
 
   if (hideButtons) {
-    buttons = null;
+    button = null;
   }
 
-  let muteTimeRemaining;
+  let muteTimeRemaining: React.ReactNode;
 
   if (account?.mute_expires_at) {
     muteTimeRemaining = (
@@ -182,7 +215,7 @@ export const Account: React.FC<{
     );
   }
 
-  let verification;
+  let verification: React.ReactNode;
 
   const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
 
@@ -232,11 +265,17 @@ export const Account: React.FC<{
         {!minimal && children && (
           <div>
             <div>{children}</div>
-            <div className='account__relationship'>{buttons}</div>
+            <div className='account__relationship'>
+              {dropdown}
+              {button}
+            </div>
           </div>
         )}
         {!minimal && !children && (
-          <div className='account__relationship'>{buttons}</div>
+          <div className='account__relationship'>
+            {dropdown}
+            {button}
+          </div>
         )}
       </div>
 
diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx
index 9d523c7402..301ffcbb24 100644
--- a/app/javascript/mastodon/components/account_bio.tsx
+++ b/app/javascript/mastodon/components/account_bio.tsx
@@ -1,4 +1,4 @@
-import { useLinks } from 'mastodon/../hooks/useLinks';
+import { useLinks } from 'mastodon/hooks/useLinks';
 
 export const AccountBio: React.FC<{
   note: string;
diff --git a/app/javascript/mastodon/components/account_fields.tsx b/app/javascript/mastodon/components/account_fields.tsx
index e297f99e3a..4ce55f7896 100644
--- a/app/javascript/mastodon/components/account_fields.tsx
+++ b/app/javascript/mastodon/components/account_fields.tsx
@@ -1,8 +1,8 @@
 import classNames from 'classnames';
 
 import CheckIcon from '@/material-icons/400-24px/check.svg?react';
-import { useLinks } from 'mastodon/../hooks/useLinks';
 import { Icon } from 'mastodon/components/icon';
+import { useLinks } from 'mastodon/hooks/useLinks';
 import type { Account } from 'mastodon/models/account';
 
 export const AccountFields: React.FC<{
diff --git a/app/javascript/mastodon/components/alerts_controller.tsx b/app/javascript/mastodon/components/alerts_controller.tsx
new file mode 100644
index 0000000000..26749fa103
--- /dev/null
+++ b/app/javascript/mastodon/components/alerts_controller.tsx
@@ -0,0 +1,105 @@
+import { useState, useEffect } from 'react';
+
+import { useIntl } from 'react-intl';
+import type { IntlShape } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { dismissAlert } from 'mastodon/actions/alerts';
+import type {
+  Alert,
+  TranslatableString,
+  TranslatableValues,
+} from 'mastodon/models/alert';
+import { useAppSelector, useAppDispatch } from 'mastodon/store';
+
+const formatIfNeeded = (
+  intl: IntlShape,
+  message: TranslatableString,
+  values?: TranslatableValues,
+) => {
+  if (typeof message === 'object') {
+    return intl.formatMessage(message, values);
+  }
+
+  return message;
+};
+
+const Alert: React.FC<{
+  alert: Alert;
+  dismissAfter: number;
+}> = ({
+  alert: { key, title, message, values, action, onClick },
+  dismissAfter,
+}) => {
+  const dispatch = useAppDispatch();
+  const intl = useIntl();
+  const [active, setActive] = useState(false);
+
+  useEffect(() => {
+    const setActiveTimeout = setTimeout(() => {
+      setActive(true);
+    }, 1);
+
+    return () => {
+      clearTimeout(setActiveTimeout);
+    };
+  }, []);
+
+  useEffect(() => {
+    const dismissTimeout = setTimeout(() => {
+      setActive(false);
+
+      // Allow CSS transition to finish before removing from the DOM
+      setTimeout(() => {
+        dispatch(dismissAlert({ key }));
+      }, 500);
+    }, dismissAfter);
+
+    return () => {
+      clearTimeout(dismissTimeout);
+    };
+  }, [dispatch, setActive, key, dismissAfter]);
+
+  return (
+    <div
+      className={classNames('notification-bar', {
+        'notification-bar-active': active,
+      })}
+    >
+      <div className='notification-bar-wrapper'>
+        {title && (
+          <span className='notification-bar-title'>
+            {formatIfNeeded(intl, title, values)}
+          </span>
+        )}
+
+        <span className='notification-bar-message'>
+          {formatIfNeeded(intl, message, values)}
+        </span>
+
+        {action && (
+          <button className='notification-bar-action' onClick={onClick}>
+            {formatIfNeeded(intl, action, values)}
+          </button>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export const AlertsController: React.FC = () => {
+  const alerts = useAppSelector((state) => state.alerts);
+
+  if (alerts.length === 0) {
+    return null;
+  }
+
+  return (
+    <div className='notification-list'>
+      {alerts.map((alert, idx) => (
+        <Alert key={alert.key} alert={alert} dismissAfter={5000 + idx * 1000} />
+      ))}
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/components/alt_text_badge.tsx b/app/javascript/mastodon/components/alt_text_badge.tsx
index 466c5cf1bc..701cfbe8b4 100644
--- a/app/javascript/mastodon/components/alt_text_badge.tsx
+++ b/app/javascript/mastodon/components/alt_text_badge.tsx
@@ -8,7 +8,7 @@ import type {
   UsePopperOptions,
 } from 'react-overlays/esm/usePopper';
 
-import { useSelectableClick } from '@/hooks/useSelectableClick';
+import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
 
 const offset = [0, 4] as OffsetValue;
 const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
diff --git a/app/javascript/mastodon/components/animated_number.tsx b/app/javascript/mastodon/components/animated_number.tsx
index 6c1e0aaec1..db422f47ce 100644
--- a/app/javascript/mastodon/components/animated_number.tsx
+++ b/app/javascript/mastodon/components/animated_number.tsx
@@ -1,6 +1,6 @@
-import { useCallback, useState } from 'react';
+import { useEffect, useState } from 'react';
 
-import { TransitionMotion, spring } from 'react-motion';
+import { animated, useSpring, config } from '@react-spring/web';
 
 import { reduceMotion } from '../initial_state';
 
@@ -11,53 +11,49 @@ interface Props {
 }
 export const AnimatedNumber: React.FC<Props> = ({ value }) => {
   const [previousValue, setPreviousValue] = useState(value);
-  const [direction, setDirection] = useState<1 | -1>(1);
+  const direction = value > previousValue ? -1 : 1;
 
-  if (previousValue !== value) {
-    setPreviousValue(value);
-    setDirection(value > previousValue ? 1 : -1);
-  }
-
-  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
-  const willLeave = useCallback(
-    () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
-    [direction],
+  const [styles, api] = useSpring(
+    () => ({
+      from: { transform: `translateY(${100 * direction}%)` },
+      to: { transform: 'translateY(0%)' },
+      onRest() {
+        setPreviousValue(value);
+      },
+      config: { ...config.gentle, duration: 200 },
+      immediate: true, // This ensures that the animation is not played when the component is first rendered
+    }),
+    [value, previousValue],
   );
 
+  // When the value changes, start the animation
+  useEffect(() => {
+    if (value !== previousValue) {
+      void api.start({ reset: true });
+    }
+  }, [api, previousValue, value]);
+
   if (reduceMotion) {
     return <ShortNumber value={value} />;
   }
 
-  const styles = [
-    {
-      key: `${value}`,
-      data: value,
-      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
-    },
-  ];
-
   return (
-    <TransitionMotion
-      styles={styles}
-      willEnter={willEnter}
-      willLeave={willLeave}
-    >
-      {(items) => (
-        <span className='animated-number'>
-          {items.map(({ key, data, style }) => (
-            <span
-              key={key}
-              style={{
-                position:
-                  direction * (style.y ?? 0) > 0 ? 'absolute' : 'static',
-                transform: `translateY(${(style.y ?? 0) * 100}%)`,
-              }}
-            >
-              <ShortNumber value={data as number} />
-            </span>
-          ))}
-        </span>
+    <span className='animated-number'>
+      <animated.span style={styles}>
+        <ShortNumber value={value} />
+      </animated.span>
+      {value !== previousValue && (
+        <animated.span
+          style={{
+            ...styles,
+            position: 'absolute',
+            top: `${-100 * direction}%`, // Adds extra space on top of translateY
+          }}
+          role='presentation'
+        >
+          <ShortNumber value={previousValue} />
+        </animated.span>
       )}
-    </TransitionMotion>
+    </span>
   );
 };
diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx
index f61d9676de..a2dc0b782e 100644
--- a/app/javascript/mastodon/components/avatar.tsx
+++ b/app/javascript/mastodon/components/avatar.tsx
@@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
 
 import classNames from 'classnames';
 
-import { useHovering } from 'mastodon/../hooks/useHovering';
+import { useHovering } from 'mastodon/hooks/useHovering';
 import { autoPlayGif } from 'mastodon/initial_state';
 import type { Account } from 'mastodon/models/account';
 
diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx
index f98cfcc38b..0bd33fea69 100644
--- a/app/javascript/mastodon/components/avatar_overlay.tsx
+++ b/app/javascript/mastodon/components/avatar_overlay.tsx
@@ -1,8 +1,7 @@
+import { useHovering } from 'mastodon/hooks/useHovering';
+import { autoPlayGif } from 'mastodon/initial_state';
 import type { Account } from 'mastodon/models/account';
 
-import { useHovering } from '../../hooks/useHovering';
-import { autoPlayGif } from '../initial_state';
-
 interface Props {
   account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
   friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
diff --git a/app/javascript/mastodon/components/copy_icon_button.jsx b/app/javascript/mastodon/components/copy_icon_button.tsx
similarity index 62%
rename from app/javascript/mastodon/components/copy_icon_button.jsx
rename to app/javascript/mastodon/components/copy_icon_button.tsx
index 0c3c6c290b..29f5f34430 100644
--- a/app/javascript/mastodon/components/copy_icon_button.jsx
+++ b/app/javascript/mastodon/components/copy_icon_button.tsx
@@ -1,29 +1,36 @@
-import PropTypes from 'prop-types';
 import { useState, useCallback } from 'react';
 
 import { defineMessages } from 'react-intl';
 
 import classNames from 'classnames';
 
-import { useDispatch } from 'react-redux';
-
 import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
 import { showAlert } from 'mastodon/actions/alerts';
 import { IconButton } from 'mastodon/components/icon_button';
+import { useAppDispatch } from 'mastodon/store';
 
 const messages = defineMessages({
-  copied: { id: 'copy_icon_button.copied', defaultMessage: 'Copied to clipboard' },
+  copied: {
+    id: 'copy_icon_button.copied',
+    defaultMessage: 'Copied to clipboard',
+  },
 });
 
-export const CopyIconButton = ({ title, value, className }) => {
+export const CopyIconButton: React.FC<{
+  title: string;
+  value: string;
+  className: string;
+}> = ({ title, value, className }) => {
   const [copied, setCopied] = useState(false);
-  const dispatch = useDispatch();
+  const dispatch = useAppDispatch();
 
   const handleClick = useCallback(() => {
-    navigator.clipboard.writeText(value);
+    void navigator.clipboard.writeText(value);
     setCopied(true);
     dispatch(showAlert({ message: messages.copied }));
-    setTimeout(() => setCopied(false), 700);
+    setTimeout(() => {
+      setCopied(false);
+    }, 700);
   }, [setCopied, value, dispatch]);
 
   return (
@@ -31,13 +38,8 @@ export const CopyIconButton = ({ title, value, className }) => {
       className={classNames(className, copied ? 'copied' : 'copyable')}
       title={title}
       onClick={handleClick}
+      icon=''
       iconComponent={ContentCopyIcon}
     />
   );
 };
-
-CopyIconButton.propTypes = {
-  title: PropTypes.string,
-  value: PropTypes.string,
-  className: PropTypes.string,
-};
diff --git a/app/javascript/mastodon/components/copy_paste_text.tsx b/app/javascript/mastodon/components/copy_paste_text.tsx
index f888acd0f7..e6eba765ab 100644
--- a/app/javascript/mastodon/components/copy_paste_text.tsx
+++ b/app/javascript/mastodon/components/copy_paste_text.tsx
@@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl';
 import classNames from 'classnames';
 
 import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
-import { useTimeout } from 'mastodon/../hooks/useTimeout';
 import { Icon } from 'mastodon/components/icon';
+import { useTimeout } from 'mastodon/hooks/useTimeout';
 
 export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
   const inputRef = useRef<HTMLTextAreaElement>(null);
diff --git a/app/javascript/mastodon/components/counters.tsx b/app/javascript/mastodon/components/counters.tsx
index 35b0ad8d60..151b25a3f7 100644
--- a/app/javascript/mastodon/components/counters.tsx
+++ b/app/javascript/mastodon/components/counters.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import type React from 'react';
 
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/mastodon/components/domain.tsx b/app/javascript/mastodon/components/domain.tsx
index aa64f0f8c3..0ccffac482 100644
--- a/app/javascript/mastodon/components/domain.tsx
+++ b/app/javascript/mastodon/components/domain.tsx
@@ -1,24 +1,15 @@
 import { useCallback } from 'react';
 
-import { defineMessages, useIntl } from 'react-intl';
+import { FormattedMessage } from 'react-intl';
 
-import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
 import { unblockDomain } from 'mastodon/actions/domain_blocks';
 import { useAppDispatch } from 'mastodon/store';
 
-import { IconButton } from './icon_button';
-
-const messages = defineMessages({
-  unblockDomain: {
-    id: 'account.unblock_domain',
-    defaultMessage: 'Unblock domain {domain}',
-  },
-});
+import { Button } from './button';
 
 export const Domain: React.FC<{
   domain: string;
 }> = ({ domain }) => {
-  const intl = useIntl();
   const dispatch = useAppDispatch();
 
   const handleDomainUnblock = useCallback(() => {
@@ -27,20 +18,17 @@ export const Domain: React.FC<{
 
   return (
     <div className='domain'>
-      <div className='domain__wrapper'>
-        <span className='domain__domain-name'>
-          <strong>{domain}</strong>
-        </span>
+      <div className='domain__domain-name'>
+        <strong>{domain}</strong>
+      </div>
 
-        <div className='domain__buttons'>
-          <IconButton
-            active
-            icon='unlock'
-            iconComponent={LockOpenIcon}
-            title={intl.formatMessage(messages.unblockDomain, { domain })}
-            onClick={handleDomainUnblock}
+      <div className='domain__buttons'>
+        <Button onClick={handleDomainUnblock}>
+          <FormattedMessage
+            id='account.unblock_domain_short'
+            defaultMessage='Unblock'
           />
-        </div>
+        </Button>
       </div>
     </div>
   );
diff --git a/app/javascript/mastodon/components/dropdown_menu.jsx b/app/javascript/mastodon/components/dropdown_menu.jsx
deleted file mode 100644
index f2b3cf90a2..0000000000
--- a/app/javascript/mastodon/components/dropdown_menu.jsx
+++ /dev/null
@@ -1,345 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent, cloneElement, Children } from 'react';
-
-import classNames from 'classnames';
-import { withRouter } from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-import { supportsPassiveEvents } from 'detect-passive-events';
-import Overlay from 'react-overlays/Overlay';
-
-import { CircularProgress } from 'mastodon/components/circular_progress';
-import { WithRouterPropTypes } from 'mastodon/utils/react_router';
-
-import { IconButton } from './icon_button';
-
-const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
-let id = 0;
-
-class DropdownMenu extends PureComponent {
-
-  static propTypes = {
-    items: PropTypes.array.isRequired,
-    loading: PropTypes.bool,
-    scrollable: PropTypes.bool,
-    onClose: PropTypes.func.isRequired,
-    style: PropTypes.object,
-    openedViaKeyboard: PropTypes.bool,
-    renderItem: PropTypes.func,
-    renderHeader: PropTypes.func,
-    onItemClick: PropTypes.func.isRequired,
-  };
-
-  static defaultProps = {
-    style: {},
-  };
-
-  handleDocumentClick = e => {
-    if (this.node && !this.node.contains(e.target)) {
-      this.props.onClose();
-      e.stopPropagation();
-      e.preventDefault();
-    }
-  };
-
-  componentDidMount () {
-    document.addEventListener('click', this.handleDocumentClick, { capture: true });
-    document.addEventListener('keydown', this.handleKeyDown, { capture: true });
-    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
-
-    if (this.focusedItem && this.props.openedViaKeyboard) {
-      this.focusedItem.focus({ preventScroll: true });
-    }
-  }
-
-  componentWillUnmount () {
-    document.removeEventListener('click', this.handleDocumentClick, { capture: true });
-    document.removeEventListener('keydown', this.handleKeyDown, { capture: true });
-    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
-  }
-
-  setRef = c => {
-    this.node = c;
-  };
-
-  setFocusRef = c => {
-    this.focusedItem = c;
-  };
-
-  handleKeyDown = e => {
-    const items = Array.from(this.node.querySelectorAll('a, button'));
-    const index = items.indexOf(document.activeElement);
-    let element = null;
-
-    switch(e.key) {
-    case 'ArrowDown':
-      element = items[index+1] || items[0];
-      break;
-    case 'ArrowUp':
-      element = items[index-1] || items[items.length-1];
-      break;
-    case 'Tab':
-      if (e.shiftKey) {
-        element = items[index-1] || items[items.length-1];
-      } else {
-        element = items[index+1] || items[0];
-      }
-      break;
-    case 'Home':
-      element = items[0];
-      break;
-    case 'End':
-      element = items[items.length-1];
-      break;
-    case 'Escape':
-      this.props.onClose();
-      break;
-    }
-
-    if (element) {
-      element.focus();
-      e.preventDefault();
-      e.stopPropagation();
-    }
-  };
-
-  handleItemKeyPress = e => {
-    if (e.key === 'Enter' || e.key === ' ') {
-      this.handleClick(e);
-    }
-  };
-
-  handleClick = e => {
-    const { onItemClick } = this.props;
-    onItemClick(e);
-  };
-
-  renderItem = (option, i) => {
-    if (option === null) {
-      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
-    }
-
-    const { text, href = '#', target = '_blank', method, dangerous } = option;
-
-    return (
-      <li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}>
-        <a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
-          {text}
-        </a>
-      </li>
-    );
-  };
-
-  render () {
-    const { items, scrollable, renderHeader, loading } = this.props;
-
-    let renderItem = this.props.renderItem || this.renderItem;
-
-    return (
-      <div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })} ref={this.setRef}>
-        {loading && (
-          <CircularProgress size={30} strokeWidth={3.5} />
-        )}
-
-        {!loading && renderHeader && (
-          <div className='dropdown-menu__container__header'>
-            {renderHeader(items)}
-          </div>
-        )}
-
-        {!loading && (
-          <ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
-            {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
-          </ul>
-        )}
-      </div>
-    );
-  }
-
-}
-
-class Dropdown extends PureComponent {
-
-  static propTypes = {
-    children: PropTypes.node,
-    icon: PropTypes.string,
-    iconComponent: PropTypes.func,
-    items: PropTypes.array.isRequired,
-    loading: PropTypes.bool,
-    size: PropTypes.number,
-    title: PropTypes.string,
-    disabled: PropTypes.bool,
-    scrollable: PropTypes.bool,
-    active: PropTypes.bool,
-    status: ImmutablePropTypes.map,
-    isUserTouching: PropTypes.func,
-    onOpen: PropTypes.func.isRequired,
-    onClose: PropTypes.func.isRequired,
-    openDropdownId: PropTypes.number,
-    openedViaKeyboard: PropTypes.bool,
-    renderItem: PropTypes.func,
-    renderHeader: PropTypes.func,
-    onItemClick: PropTypes.func,
-    ...WithRouterPropTypes
-  };
-
-  static defaultProps = {
-    title: 'Menu',
-  };
-
-  state = {
-    id: id++,
-  };
-
-  handleClick = ({ type }) => {
-    if (this.state.id === this.props.openDropdownId) {
-      this.handleClose();
-    } else {
-      this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click');
-    }
-  };
-
-  handleClose = () => {
-    if (this.activeElement) {
-      this.activeElement.focus({ preventScroll: true });
-      this.activeElement = null;
-    }
-    this.props.onClose(this.state.id);
-  };
-
-  handleMouseDown = () => {
-    if (!this.state.open) {
-      this.activeElement = document.activeElement;
-    }
-  };
-
-  handleButtonKeyDown = (e) => {
-    switch(e.key) {
-    case ' ':
-    case 'Enter':
-      this.handleMouseDown();
-      break;
-    }
-  };
-
-  handleKeyPress = (e) => {
-    switch(e.key) {
-    case ' ':
-    case 'Enter':
-      this.handleClick(e);
-      e.stopPropagation();
-      e.preventDefault();
-      break;
-    }
-  };
-
-  handleItemClick = e => {
-    const { onItemClick } = this.props;
-    const i = Number(e.currentTarget.getAttribute('data-index'));
-    const item = this.props.items[i];
-
-    this.handleClose();
-
-    if (typeof onItemClick === 'function') {
-      e.preventDefault();
-      onItemClick(item, i);
-    } else if (item && typeof item.action === 'function') {
-      e.preventDefault();
-      item.action();
-    } else if (item && item.to) {
-      e.preventDefault();
-      this.props.history.push(item.to);
-    }
-  };
-
-  setTargetRef = c => {
-    this.target = c;
-  };
-
-  findTarget = () => {
-    return this.target?.buttonRef?.current ?? this.target;
-  };
-
-  componentWillUnmount = () => {
-    if (this.state.id === this.props.openDropdownId) {
-      this.handleClose();
-    }
-  };
-
-  close = () => {
-    this.handleClose();
-  };
-
-  render () {
-    const {
-      icon,
-      iconComponent,
-      items,
-      size,
-      title,
-      disabled,
-      loading,
-      scrollable,
-      openDropdownId,
-      openedViaKeyboard,
-      children,
-      renderItem,
-      renderHeader,
-      active,
-    } = this.props;
-
-    const open = this.state.id === openDropdownId;
-
-    const button = children ? cloneElement(Children.only(children), {
-      onClick: this.handleClick,
-      onMouseDown: this.handleMouseDown,
-      onKeyDown: this.handleButtonKeyDown,
-      onKeyPress: this.handleKeyPress,
-      ref: this.setTargetRef,
-    }) : (
-      <IconButton
-        icon={!open ? icon : 'close'}
-        iconComponent={iconComponent}
-        title={title}
-        active={open || active}
-        disabled={disabled}
-        size={size}
-        onClick={this.handleClick}
-        onMouseDown={this.handleMouseDown}
-        onKeyDown={this.handleButtonKeyDown}
-        onKeyPress={this.handleKeyPress}
-        ref={this.setTargetRef}
-      />
-    );
-
-    return (
-      <>
-        {button}
-
-        <Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
-          {({ props, arrowProps, placement }) => (
-            <div {...props}>
-              <div className={`dropdown-animation dropdown-menu ${placement}`}>
-                <div className={`dropdown-menu__arrow ${placement}`} {...arrowProps} />
-                <DropdownMenu
-                  items={items}
-                  loading={loading}
-                  scrollable={scrollable}
-                  onClose={this.handleClose}
-                  openedViaKeyboard={openedViaKeyboard}
-                  renderItem={renderItem}
-                  renderHeader={renderHeader}
-                  onItemClick={this.handleItemClick}
-                />
-              </div>
-            </div>
-          )}
-        </Overlay>
-      </>
-    );
-  }
-
-}
-
-export default withRouter(Dropdown);
diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx
new file mode 100644
index 0000000000..0f9ab5b1cc
--- /dev/null
+++ b/app/javascript/mastodon/components/dropdown_menu.tsx
@@ -0,0 +1,551 @@
+import {
+  useState,
+  useEffect,
+  useRef,
+  useCallback,
+  cloneElement,
+  Children,
+} from 'react';
+
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
+import type { Map as ImmutableMap } from 'immutable';
+
+import Overlay from 'react-overlays/Overlay';
+import type {
+  OffsetValue,
+  UsePopperOptions,
+} from 'react-overlays/esm/usePopper';
+
+import { fetchRelationships } from 'mastodon/actions/accounts';
+import {
+  openDropdownMenu,
+  closeDropdownMenu,
+} from 'mastodon/actions/dropdown_menu';
+import { openModal, closeModal } from 'mastodon/actions/modal';
+import { CircularProgress } from 'mastodon/components/circular_progress';
+import { isUserTouching } from 'mastodon/is_mobile';
+import type {
+  MenuItem,
+  ActionMenuItem,
+  ExternalLinkMenuItem,
+} from 'mastodon/models/dropdown_menu';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+import type { IconProp } from './icon';
+import { IconButton } from './icon_button';
+
+let id = 0;
+
+const isMenuItem = (item: unknown): item is MenuItem => {
+  if (item === null) {
+    return true;
+  }
+
+  return typeof item === 'object' && 'text' in item;
+};
+
+const isActionItem = (item: unknown): item is ActionMenuItem => {
+  if (!item || !isMenuItem(item)) {
+    return false;
+  }
+
+  return 'action' in item;
+};
+
+const isExternalLinkItem = (item: unknown): item is ExternalLinkMenuItem => {
+  if (!item || !isMenuItem(item)) {
+    return false;
+  }
+
+  return 'href' in item;
+};
+
+type RenderItemFn<Item = MenuItem> = (
+  item: Item,
+  index: number,
+  handlers: {
+    onClick: (e: React.MouseEvent) => void;
+    onKeyUp: (e: React.KeyboardEvent) => void;
+  },
+) => React.ReactNode;
+
+type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
+
+type RenderHeaderFn<Item = MenuItem> = (items: Item[]) => React.ReactNode;
+
+interface DropdownMenuProps<Item = MenuItem> {
+  items?: Item[];
+  loading?: boolean;
+  scrollable?: boolean;
+  onClose: () => void;
+  openedViaKeyboard: boolean;
+  renderItem?: RenderItemFn<Item>;
+  renderHeader?: RenderHeaderFn<Item>;
+  onItemClick?: ItemClickFn<Item>;
+}
+
+export const DropdownMenu = <Item = MenuItem,>({
+  items,
+  loading,
+  scrollable,
+  onClose,
+  openedViaKeyboard,
+  renderItem,
+  renderHeader,
+  onItemClick,
+}: DropdownMenuProps<Item>) => {
+  const nodeRef = useRef<HTMLDivElement>(null);
+  const focusedItemRef = useRef<HTMLElement | null>(null);
+
+  useEffect(() => {
+    const handleDocumentClick = (e: MouseEvent) => {
+      if (
+        e.target instanceof Node &&
+        nodeRef.current &&
+        !nodeRef.current.contains(e.target)
+      ) {
+        onClose();
+        e.stopPropagation();
+        e.preventDefault();
+      }
+    };
+
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (!nodeRef.current) {
+        return;
+      }
+
+      const items = Array.from(nodeRef.current.querySelectorAll('a, button'));
+      const index = document.activeElement
+        ? items.indexOf(document.activeElement)
+        : -1;
+
+      let element: Element | undefined;
+
+      switch (e.key) {
+        case 'ArrowDown':
+          element = items[index + 1] ?? items[0];
+          break;
+        case 'ArrowUp':
+          element = items[index - 1] ?? items[items.length - 1];
+          break;
+        case 'Tab':
+          if (e.shiftKey) {
+            element = items[index - 1] ?? items[items.length - 1];
+          } else {
+            element = items[index + 1] ?? items[0];
+          }
+          break;
+        case 'Home':
+          element = items[0];
+          break;
+        case 'End':
+          element = items[items.length - 1];
+          break;
+        case 'Escape':
+          onClose();
+          break;
+      }
+
+      if (element && element instanceof HTMLElement) {
+        element.focus();
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    };
+
+    document.addEventListener('click', handleDocumentClick, { capture: true });
+    document.addEventListener('keydown', handleKeyDown, { capture: true });
+
+    if (focusedItemRef.current && openedViaKeyboard) {
+      focusedItemRef.current.focus({ preventScroll: true });
+    }
+
+    return () => {
+      document.removeEventListener('click', handleDocumentClick, {
+        capture: true,
+      });
+      document.removeEventListener('keydown', handleKeyDown, { capture: true });
+    };
+  }, [onClose, openedViaKeyboard]);
+
+  const handleFocusedItemRef = useCallback(
+    (c: HTMLAnchorElement | HTMLButtonElement | null) => {
+      focusedItemRef.current = c as HTMLElement;
+    },
+    [],
+  );
+
+  const handleItemClick = useCallback(
+    (e: React.MouseEvent | React.KeyboardEvent) => {
+      const i = Number(e.currentTarget.getAttribute('data-index'));
+      const item = items?.[i];
+
+      onClose();
+
+      if (!item) {
+        return;
+      }
+
+      if (typeof onItemClick === 'function') {
+        e.preventDefault();
+        onItemClick(item, i);
+      } else if (isActionItem(item)) {
+        e.preventDefault();
+        item.action();
+      }
+    },
+    [onClose, onItemClick, items],
+  );
+
+  const handleItemKeyUp = useCallback(
+    (e: React.KeyboardEvent) => {
+      if (e.key === 'Enter' || e.key === ' ') {
+        handleItemClick(e);
+      }
+    },
+    [handleItemClick],
+  );
+
+  const nativeRenderItem = (option: Item, i: number) => {
+    if (!isMenuItem(option)) {
+      return null;
+    }
+
+    if (option === null) {
+      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
+    }
+
+    const { text, dangerous } = option;
+
+    let element: React.ReactElement;
+
+    if (isActionItem(option)) {
+      element = (
+        <button
+          ref={i === 0 ? handleFocusedItemRef : undefined}
+          onClick={handleItemClick}
+          onKeyUp={handleItemKeyUp}
+          data-index={i}
+        >
+          {text}
+        </button>
+      );
+    } else if (isExternalLinkItem(option)) {
+      element = (
+        <a
+          href={option.href}
+          target={option.target ?? '_target'}
+          data-method={option.method}
+          rel='noopener'
+          ref={i === 0 ? handleFocusedItemRef : undefined}
+          onClick={handleItemClick}
+          onKeyUp={handleItemKeyUp}
+          data-index={i}
+        >
+          {text}
+        </a>
+      );
+    } else {
+      element = (
+        <Link
+          to={option.to}
+          ref={i === 0 ? handleFocusedItemRef : undefined}
+          onClick={handleItemClick}
+          onKeyUp={handleItemKeyUp}
+          data-index={i}
+        >
+          {text}
+        </Link>
+      );
+    }
+
+    return (
+      <li
+        className={classNames('dropdown-menu__item', {
+          'dropdown-menu__item--dangerous': dangerous,
+        })}
+        key={`${text}-${i}`}
+      >
+        {element}
+      </li>
+    );
+  };
+
+  const renderItemMethod = renderItem ?? nativeRenderItem;
+
+  return (
+    <div
+      className={classNames('dropdown-menu__container', {
+        'dropdown-menu__container--loading': loading,
+      })}
+      ref={nodeRef}
+    >
+      {(loading || !items) && <CircularProgress size={30} strokeWidth={3.5} />}
+
+      {!loading && renderHeader && items && (
+        <div className='dropdown-menu__container__header'>
+          {renderHeader(items)}
+        </div>
+      )}
+
+      {!loading && items && (
+        <ul
+          className={classNames('dropdown-menu__container__list', {
+            'dropdown-menu__container__list--scrollable': scrollable,
+          })}
+        >
+          {items.map((option, i) =>
+            renderItemMethod(option, i, {
+              onClick: handleItemClick,
+              onKeyUp: handleItemKeyUp,
+            }),
+          )}
+        </ul>
+      )}
+    </div>
+  );
+};
+
+interface DropdownProps<Item = MenuItem> {
+  children?: React.ReactElement;
+  icon?: string;
+  iconComponent?: IconProp;
+  items?: Item[];
+  loading?: boolean;
+  title?: string;
+  disabled?: boolean;
+  scrollable?: boolean;
+  active?: boolean;
+  scrollKey?: string;
+  status?: ImmutableMap<string, unknown>;
+  renderItem?: RenderItemFn<Item>;
+  renderHeader?: RenderHeaderFn<Item>;
+  onOpen?: () => void;
+  onItemClick?: ItemClickFn<Item>;
+}
+
+const offset = [5, 5] as OffsetValue;
+const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
+
+export const Dropdown = <Item = MenuItem,>({
+  children,
+  icon,
+  iconComponent,
+  items,
+  loading,
+  title = 'Menu',
+  disabled,
+  scrollable,
+  active,
+  status,
+  renderItem,
+  renderHeader,
+  onOpen,
+  onItemClick,
+  scrollKey,
+}: DropdownProps<Item>) => {
+  const dispatch = useAppDispatch();
+  const openDropdownId = useAppSelector((state) => state.dropdownMenu.openId);
+  const openedViaKeyboard = useAppSelector(
+    (state) => state.dropdownMenu.keyboard,
+  );
+  const [currentId] = useState(id++);
+  const open = currentId === openDropdownId;
+  const activeElement = useRef<HTMLElement | null>(null);
+  const targetRef = useRef<HTMLButtonElement | null>(null);
+
+  const handleClose = useCallback(() => {
+    if (activeElement.current) {
+      activeElement.current.focus({ preventScroll: true });
+      activeElement.current = null;
+    }
+
+    dispatch(
+      closeModal({
+        modalType: 'ACTIONS',
+        ignoreFocus: false,
+      }),
+    );
+
+    dispatch(closeDropdownMenu({ id: currentId }));
+  }, [dispatch, currentId]);
+
+  const handleItemClick = useCallback(
+    (e: React.MouseEvent | React.KeyboardEvent) => {
+      const i = Number(e.currentTarget.getAttribute('data-index'));
+      const item = items?.[i];
+
+      handleClose();
+
+      if (!item) {
+        return;
+      }
+
+      if (typeof onItemClick === 'function') {
+        e.preventDefault();
+        onItemClick(item, i);
+      } else if (isActionItem(item)) {
+        e.preventDefault();
+        item.action();
+      }
+    },
+    [handleClose, onItemClick, items],
+  );
+
+  const handleClick = useCallback(
+    (e: React.MouseEvent | React.KeyboardEvent) => {
+      const { type } = e;
+
+      if (open) {
+        handleClose();
+      } else {
+        onOpen?.();
+
+        if (status) {
+          dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
+        }
+
+        if (isUserTouching()) {
+          dispatch(
+            openModal({
+              modalType: 'ACTIONS',
+              modalProps: {
+                status,
+                actions: items,
+                onClick: handleItemClick,
+              },
+            }),
+          );
+        } else {
+          dispatch(
+            openDropdownMenu({
+              id: currentId,
+              keyboard: type !== 'click',
+              scrollKey,
+            }),
+          );
+        }
+      }
+    },
+    [
+      dispatch,
+      currentId,
+      scrollKey,
+      onOpen,
+      handleItemClick,
+      open,
+      status,
+      items,
+      handleClose,
+    ],
+  );
+
+  const handleMouseDown = useCallback(() => {
+    if (!open && document.activeElement instanceof HTMLElement) {
+      activeElement.current = document.activeElement;
+    }
+  }, [open]);
+
+  const handleButtonKeyDown = useCallback(
+    (e: React.KeyboardEvent) => {
+      switch (e.key) {
+        case ' ':
+        case 'Enter':
+          handleMouseDown();
+          break;
+      }
+    },
+    [handleMouseDown],
+  );
+
+  const handleKeyPress = useCallback(
+    (e: React.KeyboardEvent) => {
+      switch (e.key) {
+        case ' ':
+        case 'Enter':
+          handleClick(e);
+          e.stopPropagation();
+          e.preventDefault();
+          break;
+      }
+    },
+    [handleClick],
+  );
+
+  useEffect(() => {
+    return () => {
+      if (currentId === openDropdownId) {
+        handleClose();
+      }
+    };
+  }, [currentId, openDropdownId, handleClose]);
+
+  let button: React.ReactElement;
+
+  if (children) {
+    button = cloneElement(Children.only(children), {
+      onClick: handleClick,
+      onMouseDown: handleMouseDown,
+      onKeyDown: handleButtonKeyDown,
+      onKeyPress: handleKeyPress,
+      ref: targetRef,
+    });
+  } else if (icon && iconComponent) {
+    button = (
+      <IconButton
+        icon={!open ? icon : 'close'}
+        iconComponent={iconComponent}
+        title={title}
+        active={open || active}
+        disabled={disabled}
+        onClick={handleClick}
+        onMouseDown={handleMouseDown}
+        onKeyDown={handleButtonKeyDown}
+        onKeyPress={handleKeyPress}
+        ref={targetRef}
+      />
+    );
+  } else {
+    return null;
+  }
+
+  return (
+    <>
+      {button}
+
+      <Overlay
+        show={open}
+        offset={offset}
+        placement='bottom'
+        flip
+        target={targetRef}
+        popperConfig={popperConfig}
+      >
+        {({ props, arrowProps, placement }) => (
+          <div {...props}>
+            <div className={`dropdown-animation dropdown-menu ${placement}`}>
+              <div
+                className={`dropdown-menu__arrow ${placement}`}
+                {...arrowProps}
+              />
+
+              <DropdownMenu
+                items={items}
+                loading={loading}
+                scrollable={scrollable}
+                onClose={handleClose}
+                openedViaKeyboard={openedViaKeyboard}
+                renderItem={renderItem}
+                renderHeader={renderHeader}
+                onItemClick={onItemClick}
+              />
+            </div>
+          </div>
+        )}
+      </Overlay>
+    </>
+  );
+};
diff --git a/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js
deleted file mode 100644
index 726fee9076..0000000000
--- a/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { connect } from 'react-redux';
-
-import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_menu';
-import { fetchHistory } from 'mastodon/actions/history';
-import DropdownMenu from 'mastodon/components/dropdown_menu';
-
-/**
- *
- * @param {import('mastodon/store').RootState} state
- * @param {*} props
- */
-const mapStateToProps = (state, { statusId }) => ({
-  openDropdownId: state.dropdownMenu.openId,
-  openedViaKeyboard: state.dropdownMenu.keyboard,
-  items: state.getIn(['history', statusId, 'items']),
-  loading: state.getIn(['history', statusId, 'loading']),
-});
-
-const mapDispatchToProps = (dispatch, { statusId }) => ({
-
-  onOpen (id, onItemClick, keyboard) {
-    dispatch(fetchHistory(statusId));
-    dispatch(openDropdownMenu({ id, keyboard }));
-  },
-
-  onClose (id) {
-    dispatch(closeDropdownMenu({ id }));
-  },
-
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
diff --git a/app/javascript/mastodon/components/edited_timestamp/index.jsx b/app/javascript/mastodon/components/edited_timestamp/index.jsx
deleted file mode 100644
index fbf14ec4bd..0000000000
--- a/app/javascript/mastodon/components/edited_timestamp/index.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { FormattedMessage, injectIntl } from 'react-intl';
-
-import { connect } from 'react-redux';
-
-import { openModal } from 'mastodon/actions/modal';
-import InlineAccount from 'mastodon/components/inline_account';
-import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
-
-import DropdownMenu from './containers/dropdown_menu_container';
-
-const mapDispatchToProps = (dispatch, { statusId }) => ({
-
-  onItemClick (index) {
-    dispatch(openModal({
-      modalType: 'COMPARE_HISTORY',
-      modalProps: { index, statusId },
-    }));
-  },
-
-});
-
-class EditedTimestamp extends PureComponent {
-
-  static propTypes = {
-    statusId: PropTypes.string.isRequired,
-    timestamp: PropTypes.string.isRequired,
-    intl: PropTypes.object.isRequired,
-    onItemClick: PropTypes.func.isRequired,
-  };
-
-  handleItemClick = (item, i) => {
-    const { onItemClick } = this.props;
-    onItemClick(i);
-  };
-
-  renderHeader = items => {
-    return (
-      <FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {# time} other {# times}}' values={{ count: items.size - 1 }} />
-    );
-  };
-
-  renderItem = (item, index, { onClick, onKeyPress }) => {
-    const formattedDate = <RelativeTimestamp timestamp={item.get('created_at')} short={false} />;
-    const formattedName = <InlineAccount accountId={item.get('account')} />;
-
-    const label = item.get('original') ? (
-      <FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
-    ) : (
-      <FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
-    );
-
-    return (
-      <li className='dropdown-menu__item edited-timestamp__history__item' key={item.get('created_at')}>
-        <button data-index={index} onClick={onClick} onKeyPress={onKeyPress}>{label}</button>
-      </li>
-    );
-  };
-
-  render () {
-    const { timestamp, intl, statusId } = this.props;
-
-    return (
-      <DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
-        <button className='dropdown-menu__text-button'>
-          <FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <span className='animated-number'>{intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> }} />
-        </button>
-      </DropdownMenu>
-    );
-  }
-
-}
-
-export default connect(null, mapDispatchToProps)(injectIntl(EditedTimestamp));
diff --git a/app/javascript/mastodon/components/edited_timestamp/index.tsx b/app/javascript/mastodon/components/edited_timestamp/index.tsx
new file mode 100644
index 0000000000..4a33210199
--- /dev/null
+++ b/app/javascript/mastodon/components/edited_timestamp/index.tsx
@@ -0,0 +1,140 @@
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import { fetchHistory } from 'mastodon/actions/history';
+import { openModal } from 'mastodon/actions/modal';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
+import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
+import InlineAccount from 'mastodon/components/inline_account';
+import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+type HistoryItem = ImmutableMap<string, unknown>;
+
+export const EditedTimestamp: React.FC<{
+  statusId: string;
+  timestamp: string;
+}> = ({ statusId, timestamp }) => {
+  const dispatch = useAppDispatch();
+  const items = useAppSelector(
+    (state) =>
+      (
+        state.history.getIn([statusId, 'items']) as
+          | ImmutableList<unknown>
+          | undefined
+      )?.toArray() as HistoryItem[],
+  );
+  const loading = useAppSelector(
+    (state) => state.history.getIn([statusId, 'loading']) as boolean,
+  );
+
+  const handleOpen = useCallback(() => {
+    dispatch(fetchHistory(statusId));
+  }, [dispatch, statusId]);
+
+  const handleItemClick = useCallback(
+    (_item: HistoryItem, index: number) => {
+      dispatch(
+        openModal({
+          modalType: 'COMPARE_HISTORY',
+          modalProps: { index, statusId },
+        }),
+      );
+    },
+    [dispatch, statusId],
+  );
+
+  const renderHeader = useCallback((items: HistoryItem[]) => {
+    return (
+      <FormattedMessage
+        id='status.edited_x_times'
+        defaultMessage='Edited {count, plural, one {# time} other {# times}}'
+        values={{ count: items.length - 1 }}
+      />
+    );
+  }, []);
+
+  const renderItem = useCallback(
+    (
+      item: HistoryItem,
+      index: number,
+      {
+        onClick,
+        onKeyUp,
+      }: {
+        onClick: React.MouseEventHandler;
+        onKeyUp: React.KeyboardEventHandler;
+      },
+    ) => {
+      const formattedDate = (
+        <RelativeTimestamp
+          timestamp={item.get('created_at') as string}
+          short={false}
+        />
+      );
+      const formattedName = (
+        <InlineAccount accountId={item.get('account') as string} />
+      );
+
+      const label = (item.get('original') as boolean) ? (
+        <FormattedMessage
+          id='status.history.created'
+          defaultMessage='{name} created {date}'
+          values={{ name: formattedName, date: formattedDate }}
+        />
+      ) : (
+        <FormattedMessage
+          id='status.history.edited'
+          defaultMessage='{name} edited {date}'
+          values={{ name: formattedName, date: formattedDate }}
+        />
+      );
+
+      return (
+        <li
+          className='dropdown-menu__item edited-timestamp__history__item'
+          key={item.get('created_at') as string}
+        >
+          <button data-index={index} onClick={onClick} onKeyUp={onKeyUp}>
+            {label}
+          </button>
+        </li>
+      );
+    },
+    [],
+  );
+
+  return (
+    <Dropdown<HistoryItem>
+      items={items}
+      loading={loading}
+      renderItem={renderItem}
+      scrollable
+      renderHeader={renderHeader}
+      onOpen={handleOpen}
+      onItemClick={handleItemClick}
+    >
+      <button className='dropdown-menu__text-button'>
+        <FormattedMessage
+          id='status.edited'
+          defaultMessage='Edited {date}'
+          values={{
+            date: (
+              <FormattedDateWrapper
+                className='animated-number'
+                value={timestamp}
+                month='short'
+                day='2-digit'
+                hour='2-digit'
+                minute='2-digit'
+              />
+            ),
+          }}
+        />
+      </button>
+    </Dropdown>
+  );
+};
diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx
index 083736d7fb..f21ad60240 100644
--- a/app/javascript/mastodon/components/follow_button.tsx
+++ b/app/javascript/mastodon/components/follow_button.tsx
@@ -16,8 +16,7 @@ const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
   followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
-  mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
-  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+  editProfile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
 });
 
 export const FollowButton: React.FC<{
@@ -55,7 +54,7 @@ export const FollowButton: React.FC<{
       );
     }
 
-    if (!relationship) return;
+    if (!relationship || !accountId) return;
 
     if (accountId === me) {
       return;
@@ -73,15 +72,9 @@ export const FollowButton: React.FC<{
   if (!signedIn) {
     label = intl.formatMessage(messages.follow);
   } else if (accountId === me) {
-    label = intl.formatMessage(messages.edit_profile);
+    label = intl.formatMessage(messages.editProfile);
   } else if (!relationship) {
     label = <LoadingIndicator />;
-  } else if (
-    relationship.following &&
-    isShowItem('relationships') &&
-    relationship.followed_by
-  ) {
-    label = intl.formatMessage(messages.mutual);
   } else if (relationship.following || relationship.requested) {
     label = intl.formatMessage(messages.unfollow);
   } else if (relationship.followed_by && isShowItem('relationships')) {
diff --git a/app/javascript/mastodon/components/formatted_date.tsx b/app/javascript/mastodon/components/formatted_date.tsx
new file mode 100644
index 0000000000..cc927b0873
--- /dev/null
+++ b/app/javascript/mastodon/components/formatted_date.tsx
@@ -0,0 +1,26 @@
+import type { ComponentProps } from 'react';
+
+import { FormattedDate } from 'react-intl';
+
+export const FormattedDateWrapper = (
+  props: ComponentProps<typeof FormattedDate> & { className?: string },
+) => (
+  <FormattedDate {...props}>
+    {(date) => (
+      <time dateTime={tryIsoString(props.value)} className={props.className}>
+        {date}
+      </time>
+    )}
+  </FormattedDate>
+);
+
+const tryIsoString = (date?: string | number | Date): string => {
+  if (!date) {
+    return '';
+  }
+  try {
+    return new Date(date).toISOString();
+  } catch {
+    return date.toString();
+  }
+};
diff --git a/app/javascript/mastodon/components/gif.tsx b/app/javascript/mastodon/components/gif.tsx
index 8fbcb8c76b..1cc0881a5a 100644
--- a/app/javascript/mastodon/components/gif.tsx
+++ b/app/javascript/mastodon/components/gif.tsx
@@ -1,4 +1,4 @@
-import { useHovering } from '@/hooks/useHovering';
+import { useHovering } from 'mastodon/hooks/useHovering';
 import { autoPlayGif } from 'mastodon/initial_state';
 
 export const GIF: React.FC<{
diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx
index f3d5cc1f2e..346c95183f 100644
--- a/app/javascript/mastodon/components/hashtag.tsx
+++ b/app/javascript/mastodon/components/hashtag.tsx
@@ -102,10 +102,11 @@ export interface HashtagProps {
   description?: React.ReactNode;
   history?: number[];
   name: string;
-  people: number;
+  people?: number;
   to: string;
   uses?: number;
   withGraph?: boolean;
+  children?: React.ReactNode;
 }
 
 export const Hashtag: React.FC<HashtagProps> = ({
@@ -117,6 +118,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
   className,
   description,
   withGraph = true,
+  children,
 }) => (
   <div className={classNames('trends__item', className)}>
     <div className='trends__item__name'>
@@ -151,12 +153,14 @@ export const Hashtag: React.FC<HashtagProps> = ({
           <Sparklines
             width={50}
             height={28}
-            data={history ? history : Array.from(Array(7)).map(() => 0)}
+            data={history ?? Array.from(Array(7)).map(() => 0)}
           >
             <SparklinesCurve style={{ fill: 'none' }} />
           </Sparklines>
         </SilentErrorBoundary>
       </div>
     )}
+
+    {children && <div className='trends__item__buttons'>{children}</div>}
   </div>
 );
diff --git a/app/javascript/mastodon/components/hashtag_bar.tsx b/app/javascript/mastodon/components/hashtag_bar.tsx
index 9e1d74bb74..ce8f17ddb9 100644
--- a/app/javascript/mastodon/components/hashtag_bar.tsx
+++ b/app/javascript/mastodon/components/hashtag_bar.tsx
@@ -20,6 +20,7 @@ export type StatusLike = Record<{
   contentHTML: string;
   media_attachments: List<unknown>;
   spoiler_text?: string;
+  account: Record<{ id: string }>;
 }>;
 
 function normalizeHashtag(hashtag: string) {
@@ -195,19 +196,36 @@ export function getHashtagBarForStatus(status: StatusLike) {
 
   return {
     statusContentProps,
-    hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
+    hashtagBar: (
+      <HashtagBar
+        hashtags={hashtagsInBar}
+        accountId={status.getIn(['account', 'id']) as string}
+      />
+    ),
   };
 }
 
-export function getFeaturedHashtagBar(acct: string, tags: string[]) {
-  return <HashtagBar acct={acct} hashtags={tags} defaultExpanded />;
+export function getFeaturedHashtagBar(
+  accountId: string,
+  acct: string,
+  tags: string[],
+) {
+  return (
+    <HashtagBar
+      acct={acct}
+      hashtags={tags}
+      accountId={accountId}
+      defaultExpanded
+    />
+  );
 }
 
 const HashtagBar: React.FC<{
   hashtags: string[];
+  accountId: string;
   acct?: string;
   defaultExpanded?: boolean;
-}> = ({ hashtags, acct, defaultExpanded }) => {
+}> = ({ hashtags, accountId, acct, defaultExpanded }) => {
   const [expanded, setExpanded] = useState(false);
   const handleClick = useCallback(() => {
     setExpanded(true);
@@ -228,6 +246,7 @@ const HashtagBar: React.FC<{
         <Link
           key={hashtag}
           to={acct ? `/@${acct}/tagged/${hashtag}` : `/tags/${hashtag}`}
+          data-menu-hashtag={accountId}
         >
           #<span>{hashtag}</span>
         </Link>
diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx
index 057ef1aaed..38c3306f30 100644
--- a/app/javascript/mastodon/components/hover_card_controller.tsx
+++ b/app/javascript/mastodon/components/hover_card_controller.tsx
@@ -8,8 +8,8 @@ import type {
   UsePopperOptions,
 } from 'react-overlays/esm/usePopper';
 
-import { useTimeout } from 'mastodon/../hooks/useTimeout';
 import { HoverCardAccount } from 'mastodon/components/hover_card_account';
+import { useTimeout } from 'mastodon/hooks/useTimeout';
 
 const offset = [-12, 4] as OffsetValue;
 const enterDelay = 750;
diff --git a/app/javascript/mastodon/components/icon_button.tsx b/app/javascript/mastodon/components/icon_button.tsx
index b7cac35960..7e0b3e7a22 100644
--- a/app/javascript/mastodon/components/icon_button.tsx
+++ b/app/javascript/mastodon/components/icon_button.tsx
@@ -1,4 +1,4 @@
-import { PureComponent, createRef } from 'react';
+import { useState, useEffect, useCallback, forwardRef } from 'react';
 
 import classNames from 'classnames';
 
@@ -15,101 +15,110 @@ interface Props {
   onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
   onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
   onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
-  active: boolean;
+  active?: boolean;
   expanded?: boolean;
   style?: React.CSSProperties;
   activeStyle?: React.CSSProperties;
-  disabled: boolean;
+  disabled?: boolean;
   inverted?: boolean;
-  animate: boolean;
-  overlay: boolean;
-  tabIndex: number;
+  animate?: boolean;
+  overlay?: boolean;
+  tabIndex?: number;
   counter?: number;
   href?: string;
-  ariaHidden: boolean;
+  ariaHidden?: boolean;
   data_id?: string;
 }
-interface States {
-  activate: boolean;
-  deactivate: boolean;
-}
-export class IconButton extends PureComponent<Props, States> {
-  buttonRef = createRef<HTMLButtonElement>();
 
-  static defaultProps = {
-    active: false,
-    disabled: false,
-    animate: false,
-    overlay: false,
-    tabIndex: 0,
-    ariaHidden: false,
-  };
-
-  state = {
-    activate: false,
-    deactivate: false,
-  };
-
-  UNSAFE_componentWillReceiveProps(nextProps: Props) {
-    if (!nextProps.animate) return;
-
-    if (this.props.active && !nextProps.active) {
-      this.setState({ activate: false, deactivate: true });
-    } else if (!this.props.active && nextProps.active) {
-      this.setState({ activate: true, deactivate: false });
-    }
-  }
-
-  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
-    e.preventDefault();
-
-    if (!this.props.disabled && this.props.onClick != null) {
-      this.props.onClick(e);
-    }
-  };
-
-  handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
-    if (this.props.onKeyPress && !this.props.disabled) {
-      this.props.onKeyPress(e);
-    }
-  };
-
-  handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
-    if (!this.props.disabled && this.props.onMouseDown) {
-      this.props.onMouseDown(e);
-    }
-  };
-
-  handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
-    if (!this.props.disabled && this.props.onKeyDown) {
-      this.props.onKeyDown(e);
-    }
-  };
-
-  render() {
-    const style = {
-      ...this.props.style,
-      ...(this.props.active ? this.props.activeStyle : {}),
-    };
-
-    const {
-      active,
+export const IconButton = forwardRef<HTMLButtonElement, Props>(
+  (
+    {
       className,
-      disabled,
       expanded,
       icon,
       iconComponent,
       inverted,
-      overlay,
-      tabIndex,
       title,
       counter,
       href,
-      ariaHidden,
-      data_id,
-    } = this.props;
+      style,
+      activeStyle,
+      onClick,
+      onKeyDown,
+      onKeyPress,
+      onMouseDown,
+      active = false,
+      disabled = false,
+      animate = false,
+      overlay = false,
+      tabIndex = 0,
+      ariaHidden = false,
+      data_id = undefined,
+    },
+    buttonRef,
+  ) => {
+    const [activate, setActivate] = useState(false);
+    const [deactivate, setDeactivate] = useState(false);
 
-    const { activate, deactivate } = this.state;
+    useEffect(() => {
+      if (!animate) {
+        return;
+      }
+
+      if (activate && !active) {
+        setActivate(false);
+        setDeactivate(true);
+      } else if (!activate && active) {
+        setActivate(true);
+        setDeactivate(false);
+      }
+    }, [setActivate, setDeactivate, animate, active, activate]);
+
+    const handleClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(
+      (e) => {
+        e.preventDefault();
+
+        if (!disabled) {
+          onClick?.(e);
+        }
+      },
+      [disabled, onClick],
+    );
+
+    const handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> =
+      useCallback(
+        (e) => {
+          if (!disabled) {
+            onKeyPress?.(e);
+          }
+        },
+        [disabled, onKeyPress],
+      );
+
+    const handleMouseDown: React.MouseEventHandler<HTMLButtonElement> =
+      useCallback(
+        (e) => {
+          if (!disabled) {
+            onMouseDown?.(e);
+          }
+        },
+        [disabled, onMouseDown],
+      );
+
+    const handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> =
+      useCallback(
+        (e) => {
+          if (!disabled) {
+            onKeyDown?.(e);
+          }
+        },
+        [disabled, onKeyDown],
+      );
+
+    const buttonStyle = {
+      ...style,
+      ...(active ? activeStyle : {}),
+    };
 
     const classes = classNames(className, 'icon-button', {
       active,
@@ -148,18 +157,20 @@ export class IconButton extends PureComponent<Props, States> {
         aria-hidden={ariaHidden}
         title={title}
         className={classes}
-        onClick={this.handleClick}
-        onMouseDown={this.handleMouseDown}
-        onKeyDown={this.handleKeyDown}
-        onKeyPress={this.handleKeyPress}
-        style={style}
+        onClick={handleClick}
+        onMouseDown={handleMouseDown}
+        onKeyDown={handleKeyDown}
+        onKeyPress={handleKeyPress} // eslint-disable-line @typescript-eslint/no-deprecated
+        style={buttonStyle}
         tabIndex={tabIndex}
         disabled={disabled}
         data-id={data_id}
-        ref={this.buttonRef}
+        ref={buttonRef}
       >
         {contents}
       </button>
     );
-  }
-}
+  },
+);
+
+IconButton.displayName = 'IconButton';
diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx
index fd8aa59b01..12cf381e5e 100644
--- a/app/javascript/mastodon/components/media_gallery.jsx
+++ b/app/javascript/mastodon/components/media_gallery.jsx
@@ -12,6 +12,7 @@ import { debounce } from 'lodash';
 
 import { AltTextBadge } from 'mastodon/components/alt_text_badge';
 import { Blurhash } from 'mastodon/components/blurhash';
+import { SpoilerButton } from 'mastodon/components/spoiler_button';
 import { formatTime } from 'mastodon/features/video';
 
 import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
@@ -38,6 +39,7 @@ class Item extends PureComponent {
 
   state = {
     loaded: false,
+    error: false,
   };
 
   handleMouseEnter = (e) => {
@@ -81,6 +83,10 @@ class Item extends PureComponent {
     this.setState({ loaded: true });
   };
 
+  handleImageError = () => {
+    this.setState({ error: true });
+  };
+
   render () {
     const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
 
@@ -164,6 +170,7 @@ class Item extends PureComponent {
             lang={lang}
             style={{ objectPosition: `${x}% ${y}%` }}
             onLoad={this.handleImageLoad}
+            onError={this.handleImageError}
           />
         </a>
       );
@@ -199,7 +206,7 @@ class Item extends PureComponent {
     }
 
     return (
-      <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
+      <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--error': this.state.error, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
         <Blurhash
           hash={attachment.get('blurhash')}
           dummy={!useBlurhash}
@@ -236,6 +243,7 @@ class MediaGallery extends PureComponent {
     autoplay: PropTypes.bool,
     onToggleVisibility: PropTypes.func,
     compact: PropTypes.bool,
+    matchedFilters: PropTypes.arrayOf(PropTypes.string),
   };
 
   state = {
@@ -306,11 +314,11 @@ class MediaGallery extends PureComponent {
   }
 
   render () {
-    const { media, lang, sensitive, defaultWidth, autoplay, compact } = this.props;
+    const { media, lang, sensitive, defaultWidth, autoplay, compact, matchedFilters } = this.props;
     const { visible } = this.state;
     const width = this.state.width || defaultWidth;
 
-    let children, spoilerButton;
+    let children;
 
     const style = {};
 
@@ -329,26 +337,6 @@ class MediaGallery extends PureComponent {
       children = media.map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />);
     }
 
-    if (uncached) {
-      spoilerButton = (
-        <button type='button' disabled className='spoiler-button__overlay'>
-          <span className='spoiler-button__overlay__label'>
-            <FormattedMessage id='status.uncached_media_warning' defaultMessage='Preview not available' />
-            <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.open' defaultMessage='Click to open' /></span>
-          </span>
-        </button>
-      );
-    } else if (!visible) {
-      spoilerButton = (
-        <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
-          <span className='spoiler-button__overlay__label'>
-            {sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}
-            <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
-          </span>
-        </button>
-      );
-    }
-
     const rowClass = (size === 5 || size === 6 || size === 9 || size === 10 || size === 11 || size === 12) ? 'media-gallery--row3' :
       (size === 7 || size === 8 || size === 13 || size === 14 || size === 15 || size === 16) ? 'media-gallery--row4' :
         'media-gallery--row2';
@@ -366,11 +354,7 @@ class MediaGallery extends PureComponent {
       <div className={classNames(classList)} style={style} ref={this.handleRef}>
         {children}
 
-        {(!visible || uncached) && (
-          <div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
-            {spoilerButton}
-          </div>
-        )}
+        {(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} matchedFilters={matchedFilters} />}
 
         {(visible && !uncached) && (
           <div className='media-gallery__actions'>
diff --git a/app/javascript/mastodon/components/navigation_portal.tsx b/app/javascript/mastodon/components/navigation_portal.tsx
index 08f91ce18a..d3ac8baa6e 100644
--- a/app/javascript/mastodon/components/navigation_portal.tsx
+++ b/app/javascript/mastodon/components/navigation_portal.tsx
@@ -1,25 +1,6 @@
-import { Switch, Route } from 'react-router-dom';
-
-import AccountNavigation from 'mastodon/features/account/navigation';
 import Trends from 'mastodon/features/getting_started/containers/trends_container';
 import { showTrends } from 'mastodon/initial_state';
 
-const DefaultNavigation: React.FC = () => (showTrends ? <Trends /> : null);
-
 export const NavigationPortal: React.FC = () => (
-  <div className='navigation-panel__portal'>
-    <Switch>
-      <Route path='/@:acct' exact component={AccountNavigation} />
-      <Route
-        path='/@:acct/tagged/:tagged?'
-        exact
-        component={AccountNavigation}
-      />
-      <Route path='/@:acct/with_replies' exact component={AccountNavigation} />
-      <Route path='/@:acct/followers' exact component={AccountNavigation} />
-      <Route path='/@:acct/following' exact component={AccountNavigation} />
-      <Route path='/@:acct/media' exact component={AccountNavigation} />
-      <Route component={DefaultNavigation} />
-    </Switch>
-  </div>
+  <div className='navigation-panel__portal'>{showTrends && <Trends />}</div>
 );
diff --git a/app/javascript/mastodon/components/poll.jsx b/app/javascript/mastodon/components/poll.jsx
deleted file mode 100644
index 1326131009..0000000000
--- a/app/javascript/mastodon/components/poll.jsx
+++ /dev/null
@@ -1,248 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import classNames from 'classnames';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import escapeTextContentForBrowser from 'escape-html';
-import spring from 'react-motion/lib/spring';
-
-import CheckIcon from '@/material-icons/400-24px/check.svg?react';
-import { Icon }  from 'mastodon/components/icon';
-import emojify from 'mastodon/features/emoji/emoji';
-import Motion from 'mastodon/features/ui/util/optional_motion';
-import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
-
-import { RelativeTimestamp } from './relative_timestamp';
-
-const messages = defineMessages({
-  closed: {
-    id: 'poll.closed',
-    defaultMessage: 'Closed',
-  },
-  voted: {
-    id: 'poll.voted',
-    defaultMessage: 'You voted for this answer',
-  },
-  votes: {
-    id: 'poll.votes',
-    defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
-  },
-});
-
-class Poll extends ImmutablePureComponent {
-  static propTypes = {
-    identity: identityContextPropShape,
-    poll: ImmutablePropTypes.record.isRequired,
-    status: ImmutablePropTypes.map.isRequired,
-    lang: PropTypes.string,
-    intl: PropTypes.object.isRequired,
-    disabled: PropTypes.bool,
-    refresh: PropTypes.func,
-    onVote: PropTypes.func,
-    onInteractionModal: PropTypes.func,
-  };
-
-  state = {
-    selected: {},
-    expired: null,
-  };
-
-  static getDerivedStateFromProps (props, state) {
-    const { poll } = props;
-    const expires_at = poll.get('expires_at');
-    const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
-    return (expired === state.expired) ? null : { expired };
-  }
-
-  componentDidMount () {
-    this._setupTimer();
-  }
-
-  componentDidUpdate () {
-    this._setupTimer();
-  }
-
-  componentWillUnmount () {
-    clearTimeout(this._timer);
-  }
-
-  _setupTimer () {
-    const { poll } = this.props;
-    clearTimeout(this._timer);
-    if (!this.state.expired) {
-      const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
-      this._timer = setTimeout(() => {
-        this.setState({ expired: true });
-      }, delay);
-    }
-  }
-
-  _toggleOption = value => {
-    if (this.props.poll.get('multiple')) {
-      const tmp = { ...this.state.selected };
-      if (tmp[value]) {
-        delete tmp[value];
-      } else {
-        tmp[value] = true;
-      }
-      this.setState({ selected: tmp });
-    } else {
-      const tmp = {};
-      tmp[value] = true;
-      this.setState({ selected: tmp });
-    }
-  };
-
-  handleOptionChange = ({ target: { value } }) => {
-    this._toggleOption(value);
-  };
-
-  handleOptionKeyPress = (e) => {
-    if (e.key === 'Enter' || e.key === ' ') {
-      this._toggleOption(e.target.getAttribute('data-index'));
-      e.stopPropagation();
-      e.preventDefault();
-    }
-  };
-
-  handleVote = () => {
-    if (this.props.disabled) {
-      return;
-    }
-
-    if (this.props.identity.signedIn) {
-      this.props.onVote(Object.keys(this.state.selected));
-    } else {
-      this.props.onInteractionModal('vote', this.props.status);
-    }
-  };
-
-  handleRefresh = () => {
-    if (this.props.disabled) {
-      return;
-    }
-
-    this.props.refresh();
-  };
-
-  handleReveal = () => {
-    this.setState({ revealed: true });
-  };
-
-  renderOption (option, optionIndex, showResults) {
-    const { poll, lang, disabled, intl } = this.props;
-    const pollVotesCount  = poll.get('voters_count') || poll.get('votes_count');
-    const percent         = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
-    const leading         = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
-    const active          = !!this.state.selected[`${optionIndex}`];
-    const voted           = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
-
-    const title = option.getIn(['translation', 'title']) || option.get('title');
-    let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
-
-    if (!titleHtml) {
-      const emojiMap = emojiMap(poll);
-      titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
-    }
-
-    return (
-      <li key={option.get('title')}>
-        <label className={classNames('poll__option', { selectable: !showResults })}>
-          <input
-            name='vote-options'
-            type={poll.get('multiple') ? 'checkbox' : 'radio'}
-            value={optionIndex}
-            checked={active}
-            onChange={this.handleOptionChange}
-            disabled={disabled}
-          />
-
-          {!showResults && (
-            <span
-              className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
-              tabIndex={0}
-              role={poll.get('multiple') ? 'checkbox' : 'radio'}
-              onKeyPress={this.handleOptionKeyPress}
-              aria-checked={active}
-              aria-label={title}
-              lang={lang}
-              data-index={optionIndex}
-            />
-          )}
-          {showResults && (
-            <span
-              className='poll__number'
-              title={intl.formatMessage(messages.votes, {
-                votes: option.get('votes_count'),
-              })}
-            >
-              {Math.round(percent)}%
-            </span>
-          )}
-
-          <span
-            className='poll__option__text translate'
-            lang={lang}
-            dangerouslySetInnerHTML={{ __html: titleHtml }}
-          />
-
-          {!!voted && <span className='poll__voted'>
-            <Icon id='check' icon={CheckIcon} className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
-          </span>}
-        </label>
-
-        {showResults && (
-          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
-            {({ width }) =>
-              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
-            }
-          </Motion>
-        )}
-      </li>
-    );
-  }
-
-  render () {
-    const { poll, intl } = this.props;
-    const { revealed, expired } = this.state;
-
-    if (!poll) {
-      return null;
-    }
-
-    const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
-    const showResults   = poll.get('voted') || revealed || expired;
-    const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
-
-    let votesCount = null;
-
-    if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
-      votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
-    } else {
-      votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
-    }
-
-    return (
-      <div className='poll'>
-        <ul>
-          {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
-        </ul>
-
-        <div className='poll__footer'>
-          {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
-          {!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
-          {showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
-          {votesCount}
-          {poll.get('expires_at') && <> · {timeRemaining}</>}
-        </div>
-      </div>
-    );
-  }
-
-}
-
-export default injectIntl(withIdentity(Poll));
diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx
new file mode 100644
index 0000000000..6692f674d4
--- /dev/null
+++ b/app/javascript/mastodon/components/poll.tsx
@@ -0,0 +1,337 @@
+import type { KeyboardEventHandler } from 'react';
+import { useCallback, useMemo, useState } from 'react';
+
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { animated, useSpring } from '@react-spring/web';
+import escapeTextContentForBrowser from 'escape-html';
+
+import CheckIcon from '@/material-icons/400-24px/check.svg?react';
+import { openModal } from 'mastodon/actions/modal';
+import { fetchPoll, vote } from 'mastodon/actions/polls';
+import { Icon } from 'mastodon/components/icon';
+import emojify from 'mastodon/features/emoji/emoji';
+import { useIdentity } from 'mastodon/identity_context';
+import { reduceMotion } from 'mastodon/initial_state';
+import { makeEmojiMap } from 'mastodon/models/custom_emoji';
+import type * as Model from 'mastodon/models/poll';
+import type { Status } from 'mastodon/models/status';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+import { RelativeTimestamp } from './relative_timestamp';
+
+const messages = defineMessages({
+  closed: {
+    id: 'poll.closed',
+    defaultMessage: 'Closed',
+  },
+  voted: {
+    id: 'poll.voted',
+    defaultMessage: 'You voted for this answer',
+  },
+  votes: {
+    id: 'poll.votes',
+    defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
+  },
+});
+
+interface PollProps {
+  pollId: string;
+  status: Status;
+  lang?: string;
+  disabled?: boolean;
+}
+
+export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
+  // Third party hooks
+  const poll = useAppSelector((state) => state.polls[pollId]);
+  const identity = useIdentity();
+  const intl = useIntl();
+  const dispatch = useAppDispatch();
+
+  // State
+  const [revealed, setRevealed] = useState(false);
+  const [selected, setSelected] = useState<Record<string, boolean>>({});
+
+  // Derived values
+  const expired = useMemo(() => {
+    if (!poll) {
+      return false;
+    }
+    const expiresAt = poll.expires_at;
+    return poll.expired || new Date(expiresAt).getTime() < Date.now();
+  }, [poll]);
+  const timeRemaining = useMemo(() => {
+    if (!poll) {
+      return null;
+    }
+    if (expired) {
+      return intl.formatMessage(messages.closed);
+    }
+    return <RelativeTimestamp timestamp={poll.expires_at} futureDate />;
+  }, [expired, intl, poll]);
+  const votesCount = useMemo(() => {
+    if (!poll) {
+      return null;
+    }
+    if (poll.voters_count) {
+      return (
+        <FormattedMessage
+          id='poll.total_people'
+          defaultMessage='{count, plural, one {# person} other {# people}}'
+          values={{ count: poll.voters_count }}
+        />
+      );
+    }
+    return (
+      <FormattedMessage
+        id='poll.total_votes'
+        defaultMessage='{count, plural, one {# vote} other {# votes}}'
+        values={{ count: poll.votes_count }}
+      />
+    );
+  }, [poll]);
+
+  const voteDisabled =
+    disabled || Object.values(selected).every((item) => !item);
+
+  // Event handlers
+  const handleVote = useCallback(() => {
+    if (voteDisabled) {
+      return;
+    }
+
+    if (identity.signedIn) {
+      void dispatch(vote({ pollId, choices: Object.keys(selected) }));
+    } else {
+      dispatch(
+        openModal({
+          modalType: 'INTERACTION',
+          modalProps: {
+            type: 'vote',
+            accountId: status.getIn(['account', 'id']),
+            url: status.get('uri'),
+          },
+        }),
+      );
+    }
+  }, [voteDisabled, dispatch, identity, pollId, selected, status]);
+
+  const handleReveal = useCallback(() => {
+    setRevealed(true);
+  }, []);
+
+  const handleRefresh = useCallback(() => {
+    if (disabled) {
+      return;
+    }
+    void dispatch(fetchPoll({ pollId }));
+  }, [disabled, dispatch, pollId]);
+
+  const handleOptionChange = useCallback(
+    (choiceIndex: number) => {
+      if (!poll) {
+        return;
+      }
+      if (poll.multiple) {
+        setSelected((prev) => ({
+          ...prev,
+          [choiceIndex]: !prev[choiceIndex],
+        }));
+      } else {
+        setSelected({ [choiceIndex]: true });
+      }
+    },
+    [poll],
+  );
+
+  if (!poll) {
+    return null;
+  }
+  const showResults = poll.voted || revealed || expired;
+
+  return (
+    <div className='poll'>
+      <ul>
+        {poll.options.map((option, i) => (
+          <PollOption
+            key={option.title || i}
+            index={i}
+            poll={poll}
+            option={option}
+            showResults={showResults}
+            active={!!selected[i]}
+            onChange={handleOptionChange}
+          />
+        ))}
+      </ul>
+
+      <div className='poll__footer'>
+        {!showResults && (
+          <button
+            className='button button-secondary'
+            disabled={voteDisabled}
+            onClick={handleVote}
+          >
+            <FormattedMessage id='poll.vote' defaultMessage='Vote' />
+          </button>
+        )}
+        {!showResults && (
+          <>
+            <button className='poll__link' onClick={handleReveal}>
+              <FormattedMessage id='poll.reveal' defaultMessage='See results' />
+            </button>{' '}
+            ·{' '}
+          </>
+        )}
+        {showResults && !disabled && (
+          <>
+            <button className='poll__link' onClick={handleRefresh}>
+              <FormattedMessage id='poll.refresh' defaultMessage='Refresh' />
+            </button>{' '}
+            ·{' '}
+          </>
+        )}
+        {votesCount}
+        {poll.expires_at && <> · {timeRemaining}</>}
+      </div>
+    </div>
+  );
+};
+
+type PollOptionProps = Pick<PollProps, 'disabled' | 'lang'> & {
+  active: boolean;
+  onChange: (index: number) => void;
+  poll: Model.Poll;
+  option: Model.PollOption;
+  index: number;
+  showResults?: boolean;
+};
+
+const PollOption: React.FC<PollOptionProps> = (props) => {
+  const { active, lang, disabled, poll, option, index, showResults, onChange } =
+    props;
+  const voted = option.voted || poll.own_votes?.includes(index);
+  const title = option.translation?.title ?? option.title;
+
+  const intl = useIntl();
+
+  // Derived values
+  const percent = useMemo(() => {
+    const pollVotesCount = poll.voters_count ?? poll.votes_count;
+    return pollVotesCount === 0
+      ? 0
+      : (option.votes_count / pollVotesCount) * 100;
+  }, [option, poll]);
+  const isLeading = useMemo(
+    () =>
+      poll.options
+        .filter((other) => other.title !== option.title)
+        .every((other) => option.votes_count >= other.votes_count),
+    [poll, option],
+  );
+  const titleHtml = useMemo(() => {
+    let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
+
+    if (!titleHtml) {
+      const emojiMap = makeEmojiMap(poll.emojis);
+      titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
+    }
+
+    return titleHtml;
+  }, [option, poll, title]);
+
+  // Handlers
+  const handleOptionChange = useCallback(() => {
+    onChange(index);
+  }, [index, onChange]);
+  const handleOptionKeyPress: KeyboardEventHandler = useCallback(
+    (event) => {
+      if (event.key === 'Enter' || event.key === ' ') {
+        onChange(index);
+        event.stopPropagation();
+        event.preventDefault();
+      }
+    },
+    [index, onChange],
+  );
+
+  const widthSpring = useSpring({
+    from: {
+      width: '0%',
+    },
+    to: {
+      width: `${percent}%`,
+    },
+    immediate: reduceMotion,
+  });
+
+  return (
+    <li>
+      <label
+        className={classNames('poll__option', { selectable: !showResults })}
+      >
+        <input
+          name='vote-options'
+          type={poll.multiple ? 'checkbox' : 'radio'}
+          value={index}
+          checked={active}
+          onChange={handleOptionChange}
+          disabled={disabled}
+        />
+
+        {!showResults && (
+          <span
+            className={classNames('poll__input', {
+              checkbox: poll.multiple,
+              active,
+            })}
+            tabIndex={0}
+            role={poll.multiple ? 'checkbox' : 'radio'}
+            onKeyDown={handleOptionKeyPress}
+            aria-checked={active}
+            aria-label={title}
+            lang={lang}
+            data-index={index}
+          />
+        )}
+        {showResults && (
+          <span
+            className='poll__number'
+            title={intl.formatMessage(messages.votes, {
+              votes: option.votes_count,
+            })}
+          >
+            {Math.round(percent)}%
+          </span>
+        )}
+
+        <span
+          className='poll__option__text translate'
+          lang={lang}
+          dangerouslySetInnerHTML={{ __html: titleHtml }}
+        />
+
+        {!!voted && (
+          <span className='poll__voted'>
+            <Icon
+              id='check'
+              icon={CheckIcon}
+              className='poll__voted__mark'
+              title={intl.formatMessage(messages.voted)}
+            />
+          </span>
+        )}
+      </label>
+
+      {showResults && (
+        <animated.span
+          className={classNames('poll__chart', { leading: isLeading })}
+          style={widthSpring}
+        />
+      )}
+    </li>
+  );
+};
diff --git a/app/javascript/mastodon/components/remote_hint.tsx b/app/javascript/mastodon/components/remote_hint.tsx
new file mode 100644
index 0000000000..772aa805db
--- /dev/null
+++ b/app/javascript/mastodon/components/remote_hint.tsx
@@ -0,0 +1,43 @@
+import { FormattedMessage } from 'react-intl';
+
+import { useAppSelector } from 'mastodon/store';
+
+import { TimelineHint } from './timeline_hint';
+
+interface RemoteHintProps {
+  accountId?: string;
+}
+
+export const RemoteHint: React.FC<RemoteHintProps> = ({ accountId }) => {
+  const account = useAppSelector((state) =>
+    accountId ? state.accounts.get(accountId) : undefined,
+  );
+  const domain = account?.acct ? account.acct.split('@')[1] : undefined;
+  if (
+    !account ||
+    !account.url ||
+    account.acct !== account.username ||
+    !domain
+  ) {
+    return null;
+  }
+
+  return (
+    <TimelineHint
+      url={account.url}
+      message={
+        <FormattedMessage
+          id='hints.profiles.posts_may_be_missing'
+          defaultMessage='Some posts from this profile may be missing.'
+        />
+      }
+      label={
+        <FormattedMessage
+          id='hints.profiles.see_more_posts'
+          defaultMessage='See more posts on {domain}'
+          values={{ domain: <strong>{domain}</strong> }}
+        />
+      }
+    />
+  );
+};
diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx
index 558d0307e7..815b4b59ab 100644
--- a/app/javascript/mastodon/components/router.tsx
+++ b/app/javascript/mastodon/components/router.tsx
@@ -1,5 +1,5 @@
 import type { PropsWithChildren } from 'react';
-import React from 'react';
+import type React from 'react';
 
 import { Router as OriginalRouter, useHistory } from 'react-router';
 
diff --git a/app/javascript/mastodon/components/scrollable_list.jsx b/app/javascript/mastodon/components/scrollable_list.jsx
index d463245233..93ed201a07 100644
--- a/app/javascript/mastodon/components/scrollable_list.jsx
+++ b/app/javascript/mastodon/components/scrollable_list.jsx
@@ -81,6 +81,7 @@ class ScrollableList extends PureComponent {
     bindToDocument: PropTypes.bool,
     preventScroll: PropTypes.bool,
     footer: PropTypes.node,
+    className: PropTypes.string,
   };
 
   static defaultProps = {
@@ -325,7 +326,7 @@ class ScrollableList extends PureComponent {
   };
 
   render () {
-    const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
+    const { children, scrollKey, className, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
     const { fullscreen } = this.state;
     const childrenCount = Children.count(children);
 
@@ -336,9 +337,9 @@ class ScrollableList extends PureComponent {
     if (showLoading) {
       scrollableArea = (
         <div className='scrollable scrollable--flex' ref={this.setRef}>
-          <div role='feed' className='item-list'>
-            {prepend}
-          </div>
+          {prepend}
+
+          <div role='feed' className='item-list' />
 
           <div className='scrollable__append'>
             <LoadingIndicator />
@@ -350,9 +351,9 @@ class ScrollableList extends PureComponent {
     } else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
       scrollableArea = (
         <div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
-          <div role='feed' className='item-list'>
-            {prepend}
+          {prepend}
 
+          <div role='feed' className={classNames('item-list', className)}>
             {loadPending}
 
             {Children.map(this.props.children, (child, index) => (
diff --git a/app/javascript/mastodon/components/spoiler_button.tsx b/app/javascript/mastodon/components/spoiler_button.tsx
new file mode 100644
index 0000000000..bf84ffd04d
--- /dev/null
+++ b/app/javascript/mastodon/components/spoiler_button.tsx
@@ -0,0 +1,89 @@
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+interface Props {
+  hidden?: boolean;
+  sensitive: boolean;
+  uncached?: boolean;
+  matchedFilters?: string[];
+  onClick: React.MouseEventHandler<HTMLButtonElement>;
+}
+
+export const SpoilerButton: React.FC<Props> = ({
+  hidden = false,
+  sensitive,
+  uncached = false,
+  matchedFilters,
+  onClick,
+}) => {
+  let warning;
+  let action;
+
+  if (uncached) {
+    warning = (
+      <FormattedMessage
+        id='status.uncached_media_warning'
+        defaultMessage='Preview not available'
+      />
+    );
+    action = (
+      <FormattedMessage id='status.media.open' defaultMessage='Click to open' />
+    );
+  } else if (matchedFilters) {
+    warning = (
+      <FormattedMessage
+        id='filter_warning.matches_filter'
+        defaultMessage='Matches filter “<span>{title}</span>”'
+        values={{
+          title: matchedFilters.join(', '),
+          span: (chunks) => <span className='filter-name'>{chunks}</span>,
+        }}
+      />
+    );
+    action = (
+      <FormattedMessage id='status.media.show' defaultMessage='Click to show' />
+    );
+  } else if (sensitive) {
+    warning = (
+      <FormattedMessage
+        id='status.sensitive_warning'
+        defaultMessage='Sensitive content'
+      />
+    );
+    action = (
+      <FormattedMessage id='status.media.show' defaultMessage='Click to show' />
+    );
+  } else {
+    warning = (
+      <FormattedMessage
+        id='status.media_hidden'
+        defaultMessage='Media hidden'
+      />
+    );
+    action = (
+      <FormattedMessage id='status.media.show' defaultMessage='Click to show' />
+    );
+  }
+
+  return (
+    <div
+      className={classNames('spoiler-button', {
+        'spoiler-button--hidden': hidden,
+        'spoiler-button--click-thru': uncached,
+      })}
+    >
+      <button
+        type='button'
+        className='spoiler-button__overlay'
+        onClick={onClick}
+        disabled={uncached}
+      >
+        <span className='spoiler-button__overlay__label'>
+          {warning}
+          <span className='spoiler-button__overlay__action'>{action}</span>
+        </span>
+      </button>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 5c95cf46e1..0efea48f87 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -77,7 +77,7 @@ export const defaultMediaVisibility = (status) => {
     status = status.get('reblog');
   }
 
-  return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
+  return !status.get('matched_media_filters') && (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
 };
 
 const messages = defineMessages({
@@ -496,6 +496,7 @@ class Status extends ImmutablePureComponent {
                 defaultWidth={this.props.cachedMediaWidth}
                 visible={this.state.showMedia}
                 onToggleVisibility={this.handleToggleMediaVisibility}
+                matchedFilters={status.get('matched_media_filters')}
               />
             )}
           </Bundle>
@@ -524,6 +525,7 @@ class Status extends ImmutablePureComponent {
                 blurhash={attachment.get('blurhash')}
                 visible={this.state.showMedia}
                 onToggleVisibility={this.handleToggleMediaVisibility}
+                matchedFilters={status.get('matched_media_filters')}
               />
             )}
           </Bundle>
@@ -548,6 +550,7 @@ class Status extends ImmutablePureComponent {
                 deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
                 visible={this.state.showMedia}
                 onToggleVisibility={this.handleToggleMediaVisibility}
+                matchedFilters={status.get('matched_media_filters')}
               />
             )}
           </Bundle>
diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index 5e4593160c..4721e9da93 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -25,9 +25,8 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
 import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
 import { WithRouterPropTypes } from 'mastodon/utils/react_router';
 
-
-import DropdownMenuContainer from '../containers/dropdown_menu_container';
 import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { enableEmojiReaction , bookmarkCategoryNeeded, simpleTimelineMenu, me, isHideItem, boostMenu, boostModal } from '../initial_state';
 
 import { IconButton } from './icon_button';
@@ -349,10 +348,9 @@ class StatusActionBar extends ImmutablePureComponent {
 
       if (writtenByMe && pinnableStatus) {
         menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+        menu.push(null);
       }
 
-      menu.push(null);
-
       if (writtenByMe || withDismiss) {
         menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
         menu.push(null);
@@ -498,7 +496,7 @@ class StatusActionBar extends ImmutablePureComponent {
         </div>
         <div className='status__action-bar__button-wrapper'>
           {reblogMenu.length === 0 ? reblogButton : (
-            <DropdownMenuContainer
+            <Dropdown
               className={classNames('status__action-bar__button', { reblogPrivate })}
               scrollKey={scrollKey}
               status={status}
@@ -509,9 +507,7 @@ class StatusActionBar extends ImmutablePureComponent {
               title={reblogTitle}
               active={status.get('reblogged')}
               disabled={!publicStatus && !reblogPrivate}
-            >
-              {reblogButton}
-            </DropdownMenuContainer>
+            />
           )}
         </div>
         <div className='status__action-bar__button-wrapper'>
@@ -522,7 +518,7 @@ class StatusActionBar extends ImmutablePureComponent {
         </div>
         {emojiPickerDropdown}
         <div className='status__action-bar__button-wrapper'>
-          <DropdownMenuContainer
+          <Dropdown
             scrollKey={scrollKey}
             status={status}
             items={menu}
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx
index 0c51d8cb4c..69f222f6b4 100644
--- a/app/javascript/mastodon/components/status_content.jsx
+++ b/app/javascript/mastodon/components/status_content.jsx
@@ -11,7 +11,7 @@ import { connect } from 'react-redux';
 
 import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
 import { Icon }  from 'mastodon/components/icon';
-import PollContainer from 'mastodon/containers/poll_container';
+import { Poll } from 'mastodon/components/poll';
 import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
 import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
 
@@ -115,6 +115,7 @@ class StatusContent extends PureComponent {
       } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
         link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
         link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
+        link.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id']));
       } else {
         link.setAttribute('title', link.href);
         link.classList.add('unhandled-link');
@@ -250,7 +251,7 @@ class StatusContent extends PureComponent {
     );
 
     const poll = !!status.get('poll') && (
-      <PollContainer pollId={status.get('poll')} status={status} lang={language} />
+      <Poll pollId={status.get('poll')} status={status} lang={language} />
     );
 
     if (this.props.onClick) {
diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js
deleted file mode 100644
index dc2a9648ff..0000000000
--- a/app/javascript/mastodon/containers/dropdown_menu_container.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { connect } from 'react-redux';
-
-import { fetchRelationships } from 'mastodon/actions/accounts';
-
-import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
-import { openModal, closeModal } from '../actions/modal';
-import DropdownMenu from '../components/dropdown_menu';
-import { isUserTouching } from '../is_mobile';
-
-/**
- * @param {import('mastodon/store').RootState} state
- */
-const mapStateToProps = state => ({
-  openDropdownId: state.dropdownMenu.openId,
-  openedViaKeyboard: state.dropdownMenu.keyboard,
-});
-
-/**
- * @param {any} dispatch
- * @param {Object} root0
- * @param {any} [root0.status]
- * @param {any} root0.items
- * @param {any} [root0.scrollKey]
- */
-const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
-  onOpen(id, onItemClick, keyboard) {
-    if (status) {
-      dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
-    }
-
-    dispatch(isUserTouching() ? openModal({
-      modalType: 'ACTIONS',
-      modalProps: {
-        status,
-        actions: items,
-        onClick: onItemClick,
-      },
-    }) : openDropdownMenu({ id, keyboard, scrollKey }));
-  },
-
-  onClose(id) {
-    dispatch(closeModal({
-      modalType: 'ACTIONS',
-      ignoreFocus: false,
-    }));
-    dispatch(closeDropdownMenu({ id }));
-  },
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
diff --git a/app/javascript/mastodon/containers/media_container.jsx b/app/javascript/mastodon/containers/media_container.jsx
index d18602e3b5..9c07341faa 100644
--- a/app/javascript/mastodon/containers/media_container.jsx
+++ b/app/javascript/mastodon/containers/media_container.jsx
@@ -7,12 +7,13 @@ import { fromJS } from 'immutable';
 import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
 import MediaGallery from 'mastodon/components/media_gallery';
 import ModalRoot from 'mastodon/components/modal_root';
-import Poll from 'mastodon/components/poll';
+import { Poll } from 'mastodon/components/poll';
 import Audio from 'mastodon/features/audio';
 import Card from 'mastodon/features/status/components/card';
 import MediaModal from 'mastodon/features/ui/components/media_modal';
-import Video from 'mastodon/features/video';
+import { Video } from 'mastodon/features/video';
 import { IntlProvider } from 'mastodon/locales';
+import { createPollFromServerJSON } from 'mastodon/models/poll';
 import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
 
 const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
@@ -88,7 +89,7 @@ export default class MediaContainer extends PureComponent {
             Object.assign(props, {
               ...(media   ? { media:   fromJS(media)   } : {}),
               ...(card    ? { card:    fromJS(card)    } : {}),
-              ...(poll    ? { poll:    fromJS(poll)    } : {}),
+              ...(poll    ? { poll:    createPollFromServerJSON(poll)    } : {}),
               ...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
 
               ...(componentName === 'Video' ? {
diff --git a/app/javascript/mastodon/containers/poll_container.js b/app/javascript/mastodon/containers/poll_container.js
deleted file mode 100644
index 7ca840138d..0000000000
--- a/app/javascript/mastodon/containers/poll_container.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { connect } from 'react-redux';
-
-import { debounce } from 'lodash';
-
-import { openModal } from 'mastodon/actions/modal';
-import { fetchPoll, vote } from 'mastodon/actions/polls';
-import Poll from 'mastodon/components/poll';
-
-const mapDispatchToProps = (dispatch, { pollId }) => ({
-  refresh: debounce(
-    () => {
-      dispatch(fetchPoll({ pollId }));
-    },
-    1000,
-    { leading: true },
-  ),
-
-  onVote (choices) {
-    dispatch(vote({ pollId, choices }));
-  },
-
-  onInteractionModal (type, status) {
-    dispatch(openModal({
-      modalType: 'INTERACTION',
-      modalProps: {
-        type,
-        accountId: status.getIn(['account', 'id']),
-        url: status.get('uri'),
-      },
-    }));
-  }
-});
-
-const mapStateToProps = (state, { pollId }) => ({
-  poll: state.polls.get(pollId),
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(Poll);
diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx
index 359fb2ff7a..8158f47c11 100644
--- a/app/javascript/mastodon/features/about/index.jsx
+++ b/app/javascript/mastodon/features/about/index.jsx
@@ -173,7 +173,7 @@ class About extends PureComponent {
           <div className='about__header'>
             <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
             <h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
-            <p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
+            <p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {domain}' /></p>
           </div>
 
           <div className='about__meta'>
diff --git a/app/javascript/mastodon/features/account/components/account_note.jsx b/app/javascript/mastodon/features/account/components/account_note.jsx
index e736e7ad64..df7312eafc 100644
--- a/app/javascript/mastodon/features/account/components/account_note.jsx
+++ b/app/javascript/mastodon/features/account/components/account_note.jsx
@@ -4,7 +4,6 @@ import { PureComponent } from 'react';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 import { is } from 'immutable';
-import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 import Textarea from 'react-textarea-autosize';
@@ -49,7 +48,7 @@ class InlineAlert extends PureComponent {
 class AccountNote extends ImmutablePureComponent {
 
   static propTypes = {
-    account: ImmutablePropTypes.record.isRequired,
+    accountId: PropTypes.string.isRequired,
     value: PropTypes.string,
     onSave: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
@@ -66,7 +65,7 @@ class AccountNote extends ImmutablePureComponent {
   }
 
   UNSAFE_componentWillReceiveProps (nextProps) {
-    const accountWillChange = !is(this.props.account, nextProps.account);
+    const accountWillChange = !is(this.props.accountId, nextProps.accountId);
     const newState = {};
 
     if (accountWillChange && this._isDirty()) {
@@ -102,10 +101,10 @@ class AccountNote extends ImmutablePureComponent {
     if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
       e.preventDefault();
 
-      this._save();
-
       if (this.textarea) {
         this.textarea.blur();
+      } else {
+        this._save();
       }
     } else if (e.keyCode === 27) {
       e.preventDefault();
@@ -141,21 +140,21 @@ class AccountNote extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, intl } = this.props;
+    const { accountId, intl } = this.props;
     const { value, saved } = this.state;
 
-    if (!account) {
+    if (!accountId) {
       return null;
     }
 
     return (
       <div className='account__header__account-note'>
-        <label htmlFor={`account-note-${account.get('id')}`}>
+        <label htmlFor={`account-note-${accountId}`}>
           <FormattedMessage id='account.account_note_header' defaultMessage='Personal note' /> <InlineAlert show={saved} />
         </label>
 
         <Textarea
-          id={`account-note-${account.get('id')}`}
+          id={`account-note-${accountId}`}
           className='account__header__account-note__content'
           disabled={this.props.value === null || value === null}
           placeholder={intl.formatMessage(messages.placeholder)}
diff --git a/app/javascript/mastodon/features/account/components/featured_tags.jsx b/app/javascript/mastodon/features/account/components/featured_tags.jsx
deleted file mode 100644
index 56a9efac02..0000000000
--- a/app/javascript/mastodon/features/account/components/featured_tags.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import { Hashtag } from 'mastodon/components/hashtag';
-
-const messages = defineMessages({
-  lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
-  empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' },
-});
-
-class FeaturedTags extends ImmutablePureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.record,
-    featuredTags: ImmutablePropTypes.list,
-    tagged: PropTypes.string,
-    intl: PropTypes.object.isRequired,
-  };
-
-  render () {
-    const { account, featuredTags, intl } = this.props;
-
-    if (!account || account.get('suspended') || featuredTags.isEmpty()) {
-      return null;
-    }
-
-    return (
-      <div className='getting-started__trends'>
-        <h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4>
-
-        {featuredTags.take(3).map(featuredTag => (
-          <Hashtag
-            key={featuredTag.get('name')}
-            name={featuredTag.get('name')}
-            to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
-            uses={featuredTag.get('statuses_count') * 1}
-            withGraph={false}
-            description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
-          />
-        ))}
-      </div>
-    );
-  }
-
-}
-
-export default injectIntl(FeaturedTags);
diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx
deleted file mode 100644
index 4eef8081e8..0000000000
--- a/app/javascript/mastodon/features/account/components/header.jsx
+++ /dev/null
@@ -1,566 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import classNames from 'classnames';
-import { Helmet } from 'react-helmet';
-import { NavLink, withRouter } from 'react-router-dom';
-
-import { isFulfilled, isRejected } from '@reduxjs/toolkit';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import CheckIcon from '@/material-icons/400-24px/check.svg?react';
-import LockIcon from '@/material-icons/400-24px/lock.svg?react';
-import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
-import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
-import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
-import ShareIcon from '@/material-icons/400-24px/share.svg?react';
-import { Avatar } from 'mastodon/components/avatar';
-import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
-import { Button } from 'mastodon/components/button';
-import { CopyIconButton } from 'mastodon/components/copy_icon_button';
-import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
-import { getFeaturedHashtagBar } from 'mastodon/components/hashtag_bar';
-import { Icon }  from 'mastodon/components/icon';
-import { IconButton } from 'mastodon/components/icon_button';
-import { LoadingIndicator } from 'mastodon/components/loading_indicator';
-import { ShortNumber } from 'mastodon/components/short_number';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
-import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
-import { autoPlayGif, me, domain as localDomain, isShowItem } from 'mastodon/initial_state';
-import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
-import { WithRouterPropTypes } from 'mastodon/utils/react_router';
-
-import AccountNoteContainer from '../containers/account_note_container';
-import FollowRequestNoteContainer from '../containers/follow_request_note_container';
-
-import { DomainPill } from './domain_pill';
-
-const messages = defineMessages({
-  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
-  follow: { id: 'account.follow', defaultMessage: 'Follow' },
-  followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
-  mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
-  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
-  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
-  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
-  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
-  linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
-  account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
-  mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
-  direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' },
-  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
-  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
-  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
-  report: { id: 'account.report', defaultMessage: 'Report @{name}' },
-  share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
-  copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' },
-  media: { id: 'account.media', defaultMessage: 'Media' },
-  blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
-  hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
-  showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
-  enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
-  disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
-  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
-  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
-  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
-  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
-  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
-  followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
-  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
-  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
-  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
-  endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
-  unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
-  add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
-  add_or_remove_from_antenna: { id: 'account.add_or_remove_from_antenna', defaultMessage: 'Add or Remove from antennas' },
-  add_or_remove_from_exclude_antenna: { id: 'account.add_or_remove_from_exclude_antenna', defaultMessage: 'Add or Remove from antennas as exclusion' },
-  add_or_remove_from_circle: { id: 'account.add_or_remove_from_circle', defaultMessage: 'Add or Remove from circles' },
-  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
-  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
-  languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
-  openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
-});
-
-const titleFromAccount = account => {
-  const displayName = account.get('display_name');
-  const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct');
-  const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
-
-  return `${prefix} (@${acct})`;
-};
-
-const messageForFollowButton = relationship => {
-  if(!relationship) return messages.follow;
-
-  if (relationship.get('following') && relationship.get('followed_by') && isShowItem('relationships')) {
-    return messages.mutual;
-  } else if (relationship.get('following') || relationship.get('requested')) {
-    return messages.unfollow;
-  } else if (relationship.get('followed_by') && isShowItem('relationships')) {
-    return messages.followBack;
-  } else {
-    return messages.follow;
-  }
-};
-
-const dateFormatOptions = {
-  month: 'short',
-  day: 'numeric',
-  year: 'numeric',
-  hour: '2-digit',
-  minute: '2-digit',
-};
-
-class Header extends ImmutablePureComponent {
-
-  static propTypes = {
-    identity: identityContextPropShape,
-    account: ImmutablePropTypes.record,
-    featuredTags: PropTypes.array,
-    identity_props: ImmutablePropTypes.list,
-    onFollow: PropTypes.func.isRequired,
-    onBlock: PropTypes.func.isRequired,
-    onMention: PropTypes.func.isRequired,
-    onDirect: PropTypes.func.isRequired,
-    onReblogToggle: PropTypes.func.isRequired,
-    onNotifyToggle: PropTypes.func.isRequired,
-    onReport: PropTypes.func.isRequired,
-    onMute: PropTypes.func.isRequired,
-    onBlockDomain: PropTypes.func.isRequired,
-    onUnblockDomain: PropTypes.func.isRequired,
-    onEndorseToggle: PropTypes.func.isRequired,
-    onAddToList: PropTypes.func.isRequired,
-    onAddToAntenna: PropTypes.func.isRequired,
-    onAddToExcludeAntenna: PropTypes.func.isRequired,
-    onAddToCircle: PropTypes.func.isRequired,
-    onEditAccountNote: PropTypes.func.isRequired,
-    onChangeLanguages: PropTypes.func.isRequired,
-    onInteractionModal: PropTypes.func.isRequired,
-    onOpenAvatar: PropTypes.func.isRequired,
-    onOpenURL: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-    domain: PropTypes.string.isRequired,
-    hidden: PropTypes.bool,
-    ...WithRouterPropTypes,
-  };
-
-  setRef = c => {
-    this.node = c;
-  };
-
-  openEditProfile = () => {
-    window.open('/settings/profile', '_blank');
-  };
-
-  isStatusesPageActive = (match, location) => {
-    if (!match) {
-      return false;
-    }
-
-    return !location.pathname.match(/\/(followers|following)\/?$/);
-  };
-
-  handleMouseEnter = ({ currentTarget }) => {
-    if (autoPlayGif) {
-      return;
-    }
-
-    const emojis = currentTarget.querySelectorAll('.custom-emoji');
-
-    for (var i = 0; i < emojis.length; i++) {
-      let emoji = emojis[i];
-      emoji.src = emoji.getAttribute('data-original');
-    }
-  };
-
-  handleMouseLeave = ({ currentTarget }) => {
-    if (autoPlayGif) {
-      return;
-    }
-
-    const emojis = currentTarget.querySelectorAll('.custom-emoji');
-
-    for (var i = 0; i < emojis.length; i++) {
-      let emoji = emojis[i];
-      emoji.src = emoji.getAttribute('data-static');
-    }
-  };
-
-  handleAvatarClick = e => {
-    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      this.props.onOpenAvatar();
-    }
-  };
-
-  handleShare = () => {
-    const { account } = this.props;
-
-    navigator.share({
-      url: account.get('url'),
-    }).catch((e) => {
-      if (e.name !== 'AbortError') console.error(e);
-    });
-  };
-
-  handleHashtagClick = e => {
-    const { history } = this.props;
-    const value = e.currentTarget.textContent.replace(/^#/, '');
-
-    if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      history.push(`/tags/${value}`);
-    }
-  };
-
-  handleFeaturedHashtagClick = e => {
-    const { history, account } = this.props;
-    const value = e.currentTarget.textContent.replace(/^#/, '');
-
-    if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      history.push(`/@${account.get('acct')}/tagged/${value}`);
-    }
-  };
-
-  handleMentionClick = e => {
-    const { history, onOpenURL } = this.props;
-
-    if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-
-      const link = e.currentTarget;
-
-      onOpenURL(link.href).then((result) => {
-        if (isFulfilled(result)) {
-          if (result.payload.accounts[0]) {
-            history.push(`/@${result.payload.accounts[0].acct}`);
-          } else if (result.payload.statuses[0]) {
-            history.push(`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`);
-          } else {
-            window.location = link.href;
-          }
-        } else if (isRejected(result)) {
-          window.location = link.href;
-        }
-      }).catch(() => {
-        // Nothing
-      });
-    }
-  };
-
-  _attachLinkEvents () {
-    const node = this.node;
-
-    if (!node) {
-      return;
-    }
-
-    const links = node.querySelectorAll('a');
-
-    let link;
-
-    for (var i = 0; i < links.length; ++i) {
-      link = links[i];
-
-      if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
-        if (link.href && link.href.includes('/tagged/')) {
-          link.addEventListener('click', this.handleFeaturedHashtagClick, false);
-        } else {
-          link.addEventListener('click', this.handleHashtagClick, false);
-        }
-      } else if (link.classList.contains('mention')) {
-        link.addEventListener('click', this.handleMentionClick, false);
-      }
-    }
-  }
-
-  componentDidMount () {
-    this._attachLinkEvents();
-  }
-
-  componentDidUpdate () {
-    this._attachLinkEvents();
-  }
-
-  render () {
-    const { account, hidden, intl } = this.props;
-    const { signedIn, permissions } = this.props.identity;
-
-    if (!account) {
-      return null;
-    }
-
-    const suspended    = account.get('suspended');
-    const isRemote     = account.get('acct') !== account.get('username');
-    const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null;
-
-    let actionBtn, bellBtn, lockedIcon, shareBtn;
-
-    let info = [];
-    let menu = [];
-
-    if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
-      info.push(<span key='blocked' className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
-    }
-
-    if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
-      info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
-    } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
-      info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
-    }
-
-    if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
-      bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsActiveIcon : NotificationsIcon} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
-    }
-
-    if ('share' in navigator) {
-      shareBtn = <IconButton className='optional' iconComponent={ShareIcon} title={intl.formatMessage(messages.share, { name: account.get('username') })} onClick={this.handleShare} />;
-    } else {
-      shareBtn = <CopyIconButton className='optional' title={intl.formatMessage(messages.copy)} value={account.get('url')} />;
-    }
-
-    if (me !== account.get('id')) {
-      if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
-        actionBtn = <Button disabled><LoadingIndicator /></Button>;
-      } else if (!account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
-      } else if (account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
-      }
-    } else {
-      actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
-    }
-
-    if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
-      actionBtn = '';
-    }
-
-    if (account.get('locked')) {
-      lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
-    }
-
-    if (signedIn && account.get('id') !== me && !account.get('suspended')) {
-      menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
-      menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
-      menu.push(null);
-    }
-
-    if (isRemote) {
-      menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
-      menu.push(null);
-    }
-
-    if (account.get('id') === me) {
-      menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
-      menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
-      menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
-      menu.push(null);
-      menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
-      menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
-      menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
-      menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
-      menu.push(null);
-      menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
-      menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
-      menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
-    } else if (signedIn) {
-      if (account.getIn(['relationship', 'following'])) {
-        if (!account.getIn(['relationship', 'muting'])) {
-          if (account.getIn(['relationship', 'showing_reblogs'])) {
-            menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
-          } else {
-            menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
-          }
-
-          menu.push({ text: intl.formatMessage(messages.languages), action: this.props.onChangeLanguages });
-          menu.push(null);
-        }
-
-        menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
-        menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
-      }
-      menu.push({ text: intl.formatMessage(messages.add_or_remove_from_antenna), action: this.props.onAddToAntenna });
-      menu.push({ text: intl.formatMessage(messages.add_or_remove_from_exclude_antenna), action: this.props.onAddToExcludeAntenna });
-      if (account.getIn(['relationship', 'followed_by'])) {
-        menu.push({ text: intl.formatMessage(messages.add_or_remove_from_circle), action: this.props.onAddToCircle });
-      }
-      menu.push(null);
-
-      if (account.getIn(['relationship', 'muting'])) {
-        menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
-      } else {
-        menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true });
-      }
-
-      if (account.getIn(['relationship', 'blocking'])) {
-        menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
-      } else {
-        menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
-      }
-
-      if (!account.get('suspended')) {
-        menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
-      }
-    }
-
-    if (signedIn && isRemote) {
-      menu.push(null);
-
-      if (account.getIn(['relationship', 'domain_blocking'])) {
-        menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
-      } else {
-        menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true });
-      }
-    }
-
-    if ((account.get('id') !== me && (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
-      menu.push(null);
-      if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
-        menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
-      }
-      if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
-        menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` });
-      }
-    }
-
-    const content         = { __html: account.get('note_emojified') };
-    const displayNameHtml = { __html: account.get('display_name_html') };
-    const fields          = account.get('fields');
-    const isLocal         = account.get('acct').indexOf('@') === -1;
-    const username        = account.get('acct').split('@')[0];
-    const domain          = isLocal ? localDomain : account.get('acct').split('@')[1];
-    const isIndexable     = !account.get('noindex');
-    const featuredTagsArr = this.props.featuredTags?.map((tag) => tag.get('name')).toArray() || [];
-    const featuredTags    = getFeaturedHashtagBar(account.get('acct'), featuredTagsArr);
-
-    const badges = [];
-
-    if (account.get('bot')) {
-      badges.push(<AutomatedBadge key='bot-badge' />);
-    } else if (account.get('group')) {
-      badges.push(<GroupBadge key='group-badge' />);
-    }
-
-    account.get('roles', []).forEach((role) => {
-      badges.push(<Badge key={`role-badge-${role.get('id')}`} label={<span>{role.get('name')}</span>} domain={domain} roleId={role.get('id')} />);
-    });
-
-    return (
-      <div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
-        {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />}
-
-        <div className='account__header__image'>
-          <div className='account__header__info'>
-            {info}
-          </div>
-
-          {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
-        </div>
-
-        <div className='account__header__bar'>
-          <div className='account__header__tabs'>
-            <a className='avatar' href={account.get('avatar')} rel='noopener' target='_blank' onClick={this.handleAvatarClick}>
-              <Avatar account={suspended || hidden ? undefined : account} size={90} />
-            </a>
-
-            <div className='account__header__tabs__buttons'>
-              {!hidden && bellBtn}
-              {!hidden && shareBtn}
-              <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
-              {!hidden && actionBtn}
-            </div>
-          </div>
-
-          <div className='account__header__tabs__name'>
-            <h1>
-              <span dangerouslySetInnerHTML={displayNameHtml} />
-              <small>
-                <span>@{username}<span className='invisible'>@{domain}</span></span>
-                <DomainPill username={username} domain={domain} isSelf={me === account.get('id')} />
-                {lockedIcon}
-              </small>
-            </h1>
-          </div>
-
-          {badges.length > 0 && (
-            <div className='account__header__badges'>
-              {badges}
-            </div>
-          )}
-
-          {!(suspended || hidden) && (
-            <div className='account__header__extra'>
-              <div className='account__header__bio' ref={this.setRef}>
-                {(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />}
-
-                {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
-
-                {featuredTagsArr.length > 0 && (
-                  <div className='account__header__featured-tags'>
-                    {featuredTags}
-                  </div>
-                )}
-
-                <div className='account__header__fields'>
-                  <dl>
-                    <dt><FormattedMessage id='account.joined_short' defaultMessage='Joined' /></dt>
-                    <dd>{intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' })}</dd>
-                  </dl>
-
-                  {fields.map((pair, i) => (
-                    <dl key={i} className={classNames({ verified: pair.get('verified_at') })}>
-                      <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
-
-                      <dd className='translate' title={pair.get('value_plain')}>
-                        {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
-                      </dd>
-                    </dl>
-                  ))}
-                </div>
-              </div>
-
-              <div className='account__header__extra__links'>
-                <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
-                  <ShortNumber
-                    value={account.get('statuses_count')}
-                    isHide={account.getIn(['other_settings', 'hide_statuses_count']) || false}
-                    renderer={StatusesCounter}
-                  />
-                </NavLink>
-
-                <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
-                  <ShortNumber
-                    value={account.get('following_count')}
-                    isHide={account.getIn(['other_settings', 'hide_following_count']) || false}
-                    renderer={FollowingCounter}
-                  />
-                </NavLink>
-
-                <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
-                  <ShortNumber
-                    value={account.get('followers_count')}
-                    isHide={account.getIn(['other_settings', 'hide_followers_count']) || false}
-                    renderer={FollowersCounter}
-                  />
-                </NavLink>
-              </div>
-            </div>
-          )}
-        </div>
-
-        <Helmet>
-          <title>{titleFromAccount(account)}</title>
-          <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
-          <link rel='canonical' href={account.get('url')} />
-        </Helmet>
-      </div>
-    );
-  }
-
-}
-
-export default withRouter(withIdentity(injectIntl(Header)));
diff --git a/app/javascript/mastodon/features/account/containers/account_note_container.js b/app/javascript/mastodon/features/account/containers/account_note_container.js
index 1530242d69..77de964039 100644
--- a/app/javascript/mastodon/features/account/containers/account_note_container.js
+++ b/app/javascript/mastodon/features/account/containers/account_note_container.js
@@ -4,14 +4,14 @@ import { submitAccountNote } from 'mastodon/actions/account_notes';
 
 import AccountNote from '../components/account_note';
 
-const mapStateToProps = (state, { account }) => ({
-  value: account.getIn(['relationship', 'note']),
+const mapStateToProps = (state, { accountId }) => ({
+  value: state.relationships.getIn([accountId, 'note']),
 });
 
-const mapDispatchToProps = (dispatch, { account }) => ({
+const mapDispatchToProps = (dispatch, { accountId }) => ({
 
   onSave (value) {
-    dispatch(submitAccountNote({ accountId: account.get('id'), note: value }));
+    dispatch(submitAccountNote({ accountId: accountId, note: value }));
   },
 
 });
diff --git a/app/javascript/mastodon/features/account/containers/featured_tags_container.js b/app/javascript/mastodon/features/account/containers/featured_tags_container.js
deleted file mode 100644
index 726c805f78..0000000000
--- a/app/javascript/mastodon/features/account/containers/featured_tags_container.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { List as ImmutableList } from 'immutable';
-import { connect } from 'react-redux';
-
-import { makeGetAccount } from 'mastodon/selectors';
-
-import FeaturedTags from '../components/featured_tags';
-
-const mapStateToProps = () => {
-  const getAccount = makeGetAccount();
-
-  return (state, { accountId }) => ({
-    account: getAccount(state, accountId),
-    featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()),
-  });
-};
-
-export default connect(mapStateToProps)(FeaturedTags);
diff --git a/app/javascript/mastodon/features/account/navigation.jsx b/app/javascript/mastodon/features/account/navigation.jsx
deleted file mode 100644
index aa78135de2..0000000000
--- a/app/javascript/mastodon/features/account/navigation.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { connect } from 'react-redux';
-
-import FeaturedTags from 'mastodon/features/account/containers/featured_tags_container';
-import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
-
-const mapStateToProps = (state, { match: { params: { acct } } }) => {
-  const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]);
-
-  if (!accountId) {
-    return {
-      isLoading: true,
-    };
-  }
-
-  return {
-    accountId,
-    isLoading: false,
-  };
-};
-
-class AccountNavigation extends PureComponent {
-
-  static propTypes = {
-    match: PropTypes.shape({
-      params: PropTypes.shape({
-        acct: PropTypes.string,
-        tagged: PropTypes.string,
-      }).isRequired,
-    }).isRequired,
-
-    accountId: PropTypes.string,
-    isLoading: PropTypes.bool,
-  };
-
-  render () {
-    const { accountId, isLoading, match: { params: { tagged } } } = this.props;
-
-    if (isLoading) {
-      return null;
-    }
-
-    return (
-      <FeaturedTags accountId={accountId} tagged={tagged} />
-    );
-  }
-
-}
-
-export default connect(mapStateToProps)(AccountNavigation);
diff --git a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx
new file mode 100644
index 0000000000..9dd8ffdfe0
--- /dev/null
+++ b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx
@@ -0,0 +1,50 @@
+import { FormattedMessage } from 'react-intl';
+
+import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
+
+interface EmptyMessageProps {
+  suspended: boolean;
+  hidden: boolean;
+  blockedBy: boolean;
+  accountId?: string;
+}
+
+export const EmptyMessage: React.FC<EmptyMessageProps> = ({
+  accountId,
+  suspended,
+  hidden,
+  blockedBy,
+}) => {
+  if (!accountId) {
+    return null;
+  }
+
+  let message: React.ReactNode = null;
+
+  if (suspended) {
+    message = (
+      <FormattedMessage
+        id='empty_column.account_suspended'
+        defaultMessage='Account suspended'
+      />
+    );
+  } else if (hidden) {
+    message = <LimitedAccountHint accountId={accountId} />;
+  } else if (blockedBy) {
+    message = (
+      <FormattedMessage
+        id='empty_column.account_unavailable'
+        defaultMessage='Profile unavailable'
+      />
+    );
+  } else {
+    message = (
+      <FormattedMessage
+        id='empty_column.account_featured'
+        defaultMessage='This list is empty'
+      />
+    );
+  }
+
+  return <div className='empty-column-indicator'>{message}</div>;
+};
diff --git a/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx b/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx
new file mode 100644
index 0000000000..7b476ba01d
--- /dev/null
+++ b/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx
@@ -0,0 +1,51 @@
+import { defineMessages, useIntl } from 'react-intl';
+
+import type { Map as ImmutableMap } from 'immutable';
+
+import { Hashtag } from 'mastodon/components/hashtag';
+
+export type TagMap = ImmutableMap<
+  'id' | 'name' | 'url' | 'statuses_count' | 'last_status_at' | 'accountId',
+  string | null
+>;
+
+interface FeaturedTagProps {
+  tag: TagMap;
+  account: string;
+}
+
+const messages = defineMessages({
+  lastStatusAt: {
+    id: 'account.featured_tags.last_status_at',
+    defaultMessage: 'Last post on {date}',
+  },
+  empty: {
+    id: 'account.featured_tags.last_status_never',
+    defaultMessage: 'No posts',
+  },
+});
+
+export const FeaturedTag: React.FC<FeaturedTagProps> = ({ tag, account }) => {
+  const intl = useIntl();
+  const name = tag.get('name') ?? '';
+  const count = Number.parseInt(tag.get('statuses_count') ?? '');
+  return (
+    <Hashtag
+      key={name}
+      name={name}
+      to={`/@${account}/tagged/${name}`}
+      uses={count}
+      withGraph={false}
+      description={
+        count > 0
+          ? intl.formatMessage(messages.lastStatusAt, {
+              date: intl.formatDate(tag.get('last_status_at') ?? '', {
+                month: 'short',
+                day: '2-digit',
+              }),
+            })
+          : intl.formatMessage(messages.empty)
+      }
+    />
+  );
+};
diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx
new file mode 100644
index 0000000000..70e411f61a
--- /dev/null
+++ b/app/javascript/mastodon/features/account_featured/index.tsx
@@ -0,0 +1,156 @@
+import { useEffect } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { useParams } from 'react-router';
+
+import type { Map as ImmutableMap } from 'immutable';
+import { List as ImmutableList } from 'immutable';
+
+import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
+import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines';
+import { ColumnBackButton } from 'mastodon/components/column_back_button';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import { RemoteHint } from 'mastodon/components/remote_hint';
+import StatusContainer from 'mastodon/containers/status_container';
+import { useAccountId } from 'mastodon/hooks/useAccountId';
+import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+import { AccountHeader } from '../account_timeline/components/account_header';
+import Column from '../ui/components/column';
+
+import { EmptyMessage } from './components/empty_message';
+import { FeaturedTag } from './components/featured_tag';
+import type { TagMap } from './components/featured_tag';
+
+interface Params {
+  acct?: string;
+  id?: string;
+}
+
+const AccountFeatured = () => {
+  const accountId = useAccountId();
+  const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
+  const forceEmptyState = suspended || blockedBy || hidden;
+  const { acct = '' } = useParams<Params>();
+
+  const dispatch = useAppDispatch();
+
+  useEffect(() => {
+    if (accountId) {
+      void dispatch(expandAccountFeaturedTimeline(accountId));
+      dispatch(fetchFeaturedTags(accountId));
+    }
+  }, [accountId, dispatch]);
+
+  const isLoading = useAppSelector(
+    (state) =>
+      !accountId ||
+      !!(state.timelines as ImmutableMap<string, unknown>).getIn([
+        `account:${accountId}:pinned`,
+        'isLoading',
+      ]) ||
+      !!state.user_lists.getIn(['featured_tags', accountId, 'isLoading']),
+  );
+  const featuredTags = useAppSelector(
+    (state) =>
+      state.user_lists.getIn(
+        ['featured_tags', accountId, 'items'],
+        ImmutableList(),
+      ) as ImmutableList<TagMap>,
+  );
+  const featuredStatusIds = useAppSelector(
+    (state) =>
+      (state.timelines as ImmutableMap<string, unknown>).getIn(
+        [`account:${accountId}:pinned`, 'items'],
+        ImmutableList(),
+      ) as ImmutableList<string>,
+  );
+
+  if (isLoading) {
+    return (
+      <AccountFeaturedWrapper accountId={accountId}>
+        <div className='scrollable__append'>
+          <LoadingIndicator />
+        </div>
+      </AccountFeaturedWrapper>
+    );
+  }
+
+  if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) {
+    return (
+      <AccountFeaturedWrapper accountId={accountId}>
+        <EmptyMessage
+          blockedBy={blockedBy}
+          hidden={hidden}
+          suspended={suspended}
+          accountId={accountId}
+        />
+        <RemoteHint accountId={accountId} />
+      </AccountFeaturedWrapper>
+    );
+  }
+
+  return (
+    <Column>
+      <ColumnBackButton />
+
+      <div className='scrollable scrollable--flex'>
+        {accountId && (
+          <AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
+        )}
+        {!featuredTags.isEmpty() && (
+          <>
+            <h4 className='column-subheading'>
+              <FormattedMessage
+                id='account.featured.hashtags'
+                defaultMessage='Hashtags'
+              />
+            </h4>
+            {featuredTags.map((tag) => (
+              <FeaturedTag key={tag.get('id')} tag={tag} account={acct} />
+            ))}
+          </>
+        )}
+        {!featuredStatusIds.isEmpty() && (
+          <>
+            <h4 className='column-subheading'>
+              <FormattedMessage
+                id='account.featured.posts'
+                defaultMessage='Posts'
+              />
+            </h4>
+            {featuredStatusIds.map((statusId) => (
+              <StatusContainer
+                key={`f-${statusId}`}
+                // @ts-expect-error inferred props are wrong
+                id={statusId}
+                contextType='account'
+              />
+            ))}
+          </>
+        )}
+        <RemoteHint accountId={accountId} />
+      </div>
+    </Column>
+  );
+};
+
+const AccountFeaturedWrapper = ({
+  children,
+  accountId,
+}: React.PropsWithChildren<{ accountId?: string }>) => {
+  return (
+    <Column>
+      <ColumnBackButton />
+      <div className='scrollable scrollable--flex'>
+        {accountId && <AccountHeader accountId={accountId} />}
+        {children}
+      </div>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default AccountFeatured;
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.tsx b/app/javascript/mastodon/features/account_gallery/components/media_item.tsx
index fef8a1300d..0d251ff99f 100644
--- a/app/javascript/mastodon/features/account_gallery/components/media_item.tsx
+++ b/app/javascript/mastodon/features/account_gallery/components/media_item.tsx
@@ -11,22 +11,31 @@ import { Icon } from 'mastodon/components/icon';
 import { formatTime } from 'mastodon/features/video';
 import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
 import type { Status, MediaAttachment } from 'mastodon/models/status';
+import { useAppSelector } from 'mastodon/store';
 
 export const MediaItem: React.FC<{
   attachment: MediaAttachment;
   onOpenMedia: (arg0: MediaAttachment) => void;
 }> = ({ attachment, onOpenMedia }) => {
+  const account = useAppSelector((state) =>
+    state.accounts.get(attachment.getIn(['status', 'account']) as string),
+  );
   const [visible, setVisible] = useState(
     (displayMedia !== 'hide_all' &&
       !attachment.getIn(['status', 'sensitive'])) ||
       displayMedia === 'show_all',
   );
   const [loaded, setLoaded] = useState(false);
+  const [error, setError] = useState(false);
 
   const handleImageLoad = useCallback(() => {
     setLoaded(true);
   }, [setLoaded]);
 
+  const handleImageError = useCallback(() => {
+    setError(true);
+  }, [setError]);
+
   const handleMouseEnter = useCallback(
     (e: React.MouseEvent<HTMLVideoElement>) => {
       if (e.target instanceof HTMLVideoElement) {
@@ -66,11 +75,10 @@ export const MediaItem: React.FC<{
     attachment.get('description')) as string | undefined;
   const previewUrl = attachment.get('preview_url') as string;
   const fullUrl = attachment.get('url') as string;
-  const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
+  const avatarUrl = account?.avatar_static;
   const lang = status.get('language') as string;
   const blurhash = attachment.get('blurhash') as string;
   const statusId = status.get('id') as string;
-  const acct = status.getIn(['account', 'acct']) as string;
   const type = attachment.get('type') as string;
 
   let thumbnail;
@@ -95,6 +103,7 @@ export const MediaItem: React.FC<{
           alt={description}
           lang={lang}
           onLoad={handleImageLoad}
+          onError={handleImageError}
         />
 
         <div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
@@ -115,6 +124,7 @@ export const MediaItem: React.FC<{
         lang={lang}
         style={{ objectPosition: `${x}% ${y}%` }}
         onLoad={handleImageLoad}
+        onError={handleImageError}
       />
     );
   } else if (['video', 'gifv'].includes(type)) {
@@ -170,7 +180,11 @@ export const MediaItem: React.FC<{
   }
 
   return (
-    <div className='media-gallery__item media-gallery__item--square'>
+    <div
+      className={classNames('media-gallery__item media-gallery__item--square', {
+        'media-gallery__item--error': error,
+      })}
+    >
       <Blurhash
         hash={blurhash}
         className={classNames('media-gallery__preview', {
@@ -181,7 +195,7 @@ export const MediaItem: React.FC<{
 
       <a
         className='media-gallery__item-thumbnail'
-        href={`/@${acct}/${statusId}`}
+        href={`/@${account?.acct}/${statusId}`}
         onClick={handleClick}
         target='_blank'
         rel='noopener noreferrer'
diff --git a/app/javascript/mastodon/features/account_gallery/index.jsx b/app/javascript/mastodon/features/account_gallery/index.jsx
deleted file mode 100644
index 35a0fbd2c6..0000000000
--- a/app/javascript/mastodon/features/account_gallery/index.jsx
+++ /dev/null
@@ -1,241 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { FormattedMessage } from 'react-intl';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
-import { openModal } from 'mastodon/actions/modal';
-import { ColumnBackButton } from 'mastodon/components/column_back_button';
-import { LoadMore } from 'mastodon/components/load_more';
-import { LoadingIndicator } from 'mastodon/components/loading_indicator';
-import ScrollContainer from 'mastodon/containers/scroll_container';
-import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
-import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
-import { getAccountGallery } from 'mastodon/selectors';
-
-import { expandAccountMediaTimeline } from '../../actions/timelines';
-import HeaderContainer from '../account_timeline/containers/header_container';
-import Column from '../ui/components/column';
-
-import { MediaItem } from './components/media_item';
-
-const mapStateToProps = (state, { params: { acct, id } }) => {
-  const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
-
-  if (!accountId) {
-    return {
-      isLoading: true,
-    };
-  }
-
-  return {
-    accountId,
-    isAccount: !!state.getIn(['accounts', accountId]),
-    attachments: getAccountGallery(state, accountId),
-    isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
-    hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
-    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
-    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
-  };
-};
-
-class LoadMoreMedia extends ImmutablePureComponent {
-
-  static propTypes = {
-    maxId: PropTypes.string,
-    onLoadMore: PropTypes.func.isRequired,
-  };
-
-  handleLoadMore = () => {
-    this.props.onLoadMore(this.props.maxId);
-  };
-
-  render () {
-    return (
-      <LoadMore
-        disabled={this.props.disabled}
-        onClick={this.handleLoadMore}
-      />
-    );
-  }
-
-}
-
-class AccountGallery extends ImmutablePureComponent {
-
-  static propTypes = {
-    params: PropTypes.shape({
-      acct: PropTypes.string,
-      id: PropTypes.string,
-    }).isRequired,
-    accountId: PropTypes.string,
-    dispatch: PropTypes.func.isRequired,
-    attachments: ImmutablePropTypes.list.isRequired,
-    isLoading: PropTypes.bool,
-    hasMore: PropTypes.bool,
-    isAccount: PropTypes.bool,
-    blockedBy: PropTypes.bool,
-    suspended: PropTypes.bool,
-    multiColumn: PropTypes.bool,
-  };
-
-  state = {
-    width: 323,
-  };
-
-  _load () {
-    const { accountId, isAccount, dispatch } = this.props;
-
-    if (!isAccount) dispatch(fetchAccount(accountId));
-    dispatch(expandAccountMediaTimeline(accountId));
-  }
-
-  componentDidMount () {
-    const { params: { acct }, accountId, dispatch } = this.props;
-
-    if (accountId) {
-      this._load();
-    } else {
-      dispatch(lookupAccount(acct));
-    }
-  }
-
-  componentDidUpdate (prevProps) {
-    const { params: { acct }, accountId, dispatch } = this.props;
-
-    if (prevProps.accountId !== accountId && accountId) {
-      this._load();
-    } else if (prevProps.params.acct !== acct) {
-      dispatch(lookupAccount(acct));
-    }
-  }
-
-  handleScrollToBottom = () => {
-    if (this.props.hasMore) {
-      this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
-    }
-  };
-
-  handleScroll = e => {
-    const { scrollTop, scrollHeight, clientHeight } = e.target;
-    const offset = scrollHeight - scrollTop - clientHeight;
-
-    if (150 > offset && !this.props.isLoading) {
-      this.handleScrollToBottom();
-    }
-  };
-
-  handleLoadMore = maxId => {
-    this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
-  };
-
-  handleLoadOlder = e => {
-    e.preventDefault();
-    this.handleScrollToBottom();
-  };
-
-  handleOpenMedia = attachment => {
-    const { dispatch } = this.props;
-    const statusId = attachment.getIn(['status', 'id']);
-    const lang = attachment.getIn(['status', 'language']);
-
-    if (attachment.get('type') === 'video') {
-      dispatch(openModal({
-        modalType: 'VIDEO',
-        modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
-      }));
-    } else if (attachment.get('type') === 'audio') {
-      dispatch(openModal({
-        modalType: 'AUDIO',
-        modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
-      }));
-    } else {
-      const media = attachment.getIn(['status', 'media_attachments']);
-      const index = media.findIndex(x => x.get('id') === attachment.get('id'));
-
-      dispatch(openModal({
-        modalType: 'MEDIA',
-        modalProps: { media, index, statusId, lang },
-      }));
-    }
-  };
-
-  handleRef = c => {
-    if (c) {
-      this.setState({ width: c.offsetWidth });
-    }
-  };
-
-  render () {
-    const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
-    const { width } = this.state;
-
-    if (!isAccount) {
-      return (
-        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
-      );
-    }
-
-    if (!attachments && isLoading) {
-      return (
-        <Column>
-          <LoadingIndicator />
-        </Column>
-      );
-    }
-
-    let loadOlder = null;
-
-    if (hasMore && !(isLoading && attachments.size === 0)) {
-      loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
-    }
-
-    let emptyMessage;
-
-    if (suspended) {
-      emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
-    } else if (blockedBy) {
-      emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
-    }
-
-    return (
-      <Column>
-        <ColumnBackButton />
-
-        <ScrollContainer scrollKey='account_gallery'>
-          <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
-            <HeaderContainer accountId={this.props.accountId} />
-
-            {(suspended || blockedBy) ? (
-              <div className='empty-column-indicator'>
-                {emptyMessage}
-              </div>
-            ) : (
-              <div role='feed' className='account-gallery__container' ref={this.handleRef}>
-                {attachments.map((attachment, index) => attachment === null ? (
-                  <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
-                ) : (
-                  <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
-                ))}
-
-                {loadOlder}
-              </div>
-            )}
-
-            {isLoading && attachments.size === 0 && (
-              <div className='scrollable__append'>
-                <LoadingIndicator />
-              </div>
-            )}
-          </div>
-        </ScrollContainer>
-      </Column>
-    );
-  }
-
-}
-
-export default connect(mapStateToProps)(AccountGallery);
diff --git a/app/javascript/mastodon/features/account_gallery/index.tsx b/app/javascript/mastodon/features/account_gallery/index.tsx
new file mode 100644
index 0000000000..0027329c93
--- /dev/null
+++ b/app/javascript/mastodon/features/account_gallery/index.tsx
@@ -0,0 +1,220 @@
+import { useEffect, useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { createSelector } from '@reduxjs/toolkit';
+import type { Map as ImmutableMap } from 'immutable';
+import { List as ImmutableList } from 'immutable';
+
+import { openModal } from 'mastodon/actions/modal';
+import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
+import { ColumnBackButton } from 'mastodon/components/column_back_button';
+import { RemoteHint } from 'mastodon/components/remote_hint';
+import ScrollableList from 'mastodon/components/scrollable_list';
+import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
+import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
+import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
+import Column from 'mastodon/features/ui/components/column';
+import { useAccountId } from 'mastodon/hooks/useAccountId';
+import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
+import type { MediaAttachment } from 'mastodon/models/media_attachment';
+import type { RootState } from 'mastodon/store';
+import { useAppSelector, useAppDispatch } from 'mastodon/store';
+
+import { MediaItem } from './components/media_item';
+
+const getAccountGallery = createSelector(
+  [
+    (state: RootState, accountId: string) =>
+      (state.timelines as ImmutableMap<string, unknown>).getIn(
+        [`account:${accountId}:media`, 'items'],
+        ImmutableList(),
+      ) as ImmutableList<string>,
+    (state: RootState) => state.statuses,
+  ],
+  (statusIds, statuses) => {
+    let items = ImmutableList<MediaAttachment>();
+
+    statusIds.forEach((statusId) => {
+      const status = statuses.get(statusId) as
+        | ImmutableMap<string, unknown>
+        | undefined;
+
+      if (status) {
+        items = items.concat(
+          (
+            status.get('media_attachments') as ImmutableList<MediaAttachment>
+          ).map((media) => media.set('status', status)),
+        );
+      }
+    });
+
+    return items;
+  },
+);
+
+export const AccountGallery: React.FC<{
+  multiColumn: boolean;
+}> = ({ multiColumn }) => {
+  const dispatch = useAppDispatch();
+  const accountId = useAccountId();
+  const attachments = useAppSelector((state) =>
+    accountId
+      ? getAccountGallery(state, accountId)
+      : ImmutableList<MediaAttachment>(),
+  );
+  const isLoading = useAppSelector((state) =>
+    (state.timelines as ImmutableMap<string, unknown>).getIn([
+      `account:${accountId}:media`,
+      'isLoading',
+    ]),
+  );
+  const hasMore = useAppSelector((state) =>
+    (state.timelines as ImmutableMap<string, unknown>).getIn([
+      `account:${accountId}:media`,
+      'hasMore',
+    ]),
+  );
+  const account = useAppSelector((state) =>
+    accountId ? state.accounts.get(accountId) : undefined,
+  );
+  const isAccount = !!account;
+
+  const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
+
+  const maxId = attachments.last()?.getIn(['status', 'id']) as
+    | string
+    | undefined;
+
+  useEffect(() => {
+    if (accountId && isAccount) {
+      void dispatch(expandAccountMediaTimeline(accountId));
+    }
+  }, [dispatch, accountId, isAccount]);
+
+  const handleLoadMore = useCallback(() => {
+    if (maxId) {
+      void dispatch(expandAccountMediaTimeline(accountId, { maxId }));
+    }
+  }, [dispatch, accountId, maxId]);
+
+  const handleOpenMedia = useCallback(
+    (attachment: MediaAttachment) => {
+      const statusId = attachment.getIn(['status', 'id']);
+      const lang = attachment.getIn(['status', 'language']);
+
+      if (attachment.get('type') === 'video') {
+        dispatch(
+          openModal({
+            modalType: 'VIDEO',
+            modalProps: {
+              media: attachment,
+              statusId,
+              lang,
+              options: { autoPlay: true },
+            },
+          }),
+        );
+      } else if (attachment.get('type') === 'audio') {
+        dispatch(
+          openModal({
+            modalType: 'AUDIO',
+            modalProps: {
+              media: attachment,
+              statusId,
+              lang,
+              options: { autoPlay: true },
+            },
+          }),
+        );
+      } else {
+        const media = attachment.getIn([
+          'status',
+          'media_attachments',
+        ]) as ImmutableList<MediaAttachment>;
+        const index = media.findIndex(
+          (x) => x.get('id') === attachment.get('id'),
+        );
+
+        dispatch(
+          openModal({
+            modalType: 'MEDIA',
+            modalProps: { media, index, statusId, lang },
+          }),
+        );
+      }
+    },
+    [dispatch],
+  );
+
+  if (accountId && !isAccount) {
+    return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
+  }
+
+  let emptyMessage;
+
+  if (accountId) {
+    if (suspended) {
+      emptyMessage = (
+        <FormattedMessage
+          id='empty_column.account_suspended'
+          defaultMessage='Account suspended'
+        />
+      );
+    } else if (hidden) {
+      emptyMessage = <LimitedAccountHint accountId={accountId} />;
+    } else if (blockedBy) {
+      emptyMessage = (
+        <FormattedMessage
+          id='empty_column.account_unavailable'
+          defaultMessage='Profile unavailable'
+        />
+      );
+    } else if (attachments.isEmpty()) {
+      emptyMessage = <RemoteHint accountId={accountId} />;
+    } else {
+      emptyMessage = (
+        <FormattedMessage
+          id='empty_column.account_timeline'
+          defaultMessage='No posts found'
+        />
+      );
+    }
+  }
+
+  const forceEmptyState = suspended || blockedBy || hidden;
+
+  return (
+    <Column>
+      <ColumnBackButton />
+
+      <ScrollableList
+        className='account-gallery__container'
+        prepend={
+          accountId && (
+            <AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
+          )
+        }
+        alwaysPrepend
+        append={accountId && <RemoteHint accountId={accountId} />}
+        scrollKey='account_gallery'
+        isLoading={isLoading}
+        hasMore={!forceEmptyState && hasMore}
+        onLoadMore={handleLoadMore}
+        emptyMessage={emptyMessage}
+        bindToDocument={!multiColumn}
+      >
+        {attachments.map((attachment) => (
+          <MediaItem
+            key={attachment.get('id') as string}
+            attachment={attachment}
+            onOpenMedia={handleOpenMedia}
+          />
+        ))}
+      </ScrollableList>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default AccountGallery;
diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
new file mode 100644
index 0000000000..3f9c7d843a
--- /dev/null
+++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
@@ -0,0 +1,1090 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call,
+                  @typescript-eslint/no-unsafe-return,
+                  @typescript-eslint/no-unsafe-assignment,
+                  @typescript-eslint/no-unsafe-argument,
+                  @typescript-eslint/no-unsafe-member-access,
+                  @typescript-eslint/no-explicit-any
+                  -- the settings store is not yet typed */
+import { useCallback, useMemo } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { Helmet } from 'react-helmet';
+import { NavLink } from 'react-router-dom';
+
+import CheckIcon from '@/material-icons/400-24px/check.svg?react';
+import LockIcon from '@/material-icons/400-24px/lock.svg?react';
+import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
+import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
+import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
+import ShareIcon from '@/material-icons/400-24px/share.svg?react';
+import {
+  followAccount,
+  unblockAccount,
+  unmuteAccount,
+  pinAccount,
+  unpinAccount,
+} from 'mastodon/actions/accounts';
+import { initBlockModal } from 'mastodon/actions/blocks';
+import { mentionCompose, directCompose } from 'mastodon/actions/compose';
+import {
+  initDomainBlockModal,
+  unblockDomain,
+} from 'mastodon/actions/domain_blocks';
+import { openModal } from 'mastodon/actions/modal';
+import { initMuteModal } from 'mastodon/actions/mutes';
+import { initReport } from 'mastodon/actions/reports';
+import { Avatar } from 'mastodon/components/avatar';
+import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
+import { Button } from 'mastodon/components/button';
+import { CopyIconButton } from 'mastodon/components/copy_icon_button';
+import {
+  FollowersCounter,
+  FollowingCounter,
+  StatusesCounter,
+} from 'mastodon/components/counters';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
+import { FollowButton } from 'mastodon/components/follow_button';
+import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
+import { getFeaturedHashtagBar } from 'mastodon/components/hashtag_bar';
+import { Icon } from 'mastodon/components/icon';
+import { IconButton } from 'mastodon/components/icon_button';
+import { ShortNumber } from 'mastodon/components/short_number';
+import { DomainPill } from 'mastodon/features/account/components/domain_pill';
+import AccountNoteContainer from 'mastodon/features/account/containers/account_note_container';
+import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
+import { useLinks } from 'mastodon/hooks/useLinks';
+import { useIdentity } from 'mastodon/identity_context';
+import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
+import type { Account } from 'mastodon/models/account';
+import type { MenuItem } from 'mastodon/models/dropdown_menu';
+import {
+  PERMISSION_MANAGE_USERS,
+  PERMISSION_MANAGE_FEDERATION,
+} from 'mastodon/permissions';
+import { getAccountHidden } from 'mastodon/selectors/accounts';
+import { useAppSelector, useAppDispatch } from 'mastodon/store';
+
+import { MemorialNote } from './memorial_note';
+import { MovedNote } from './moved_note';
+
+const messages = defineMessages({
+  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+  follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
+  mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
+  cancel_follow_request: {
+    id: 'account.cancel_follow_request',
+    defaultMessage: 'Withdraw follow request',
+  },
+  requested: {
+    id: 'account.requested',
+    defaultMessage: 'Awaiting approval. Click to cancel follow request',
+  },
+  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+  linkVerifiedOn: {
+    id: 'account.link_verified_on',
+    defaultMessage: 'Ownership of this link was checked on {date}',
+  },
+  account_locked: {
+    id: 'account.locked_info',
+    defaultMessage:
+      'This account privacy status is set to locked. The owner manually reviews who can follow them.',
+  },
+  mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
+  direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' },
+  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
+  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
+  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
+  report: { id: 'account.report', defaultMessage: 'Report @{name}' },
+  share: { id: 'account.share', defaultMessage: "Share @{name}'s profile" },
+  copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' },
+  media: { id: 'account.media', defaultMessage: 'Media' },
+  blockDomain: {
+    id: 'account.block_domain',
+    defaultMessage: 'Block domain {domain}',
+  },
+  unblockDomain: {
+    id: 'account.unblock_domain',
+    defaultMessage: 'Unblock domain {domain}',
+  },
+  hideReblogs: {
+    id: 'account.hide_reblogs',
+    defaultMessage: 'Hide boosts from @{name}',
+  },
+  showReblogs: {
+    id: 'account.show_reblogs',
+    defaultMessage: 'Show boosts from @{name}',
+  },
+  enableNotifications: {
+    id: 'account.enable_notifications',
+    defaultMessage: 'Notify me when @{name} posts',
+  },
+  disableNotifications: {
+    id: 'account.disable_notifications',
+    defaultMessage: 'Stop notifying me when @{name} posts',
+  },
+  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
+  preferences: {
+    id: 'navigation_bar.preferences',
+    defaultMessage: 'Preferences',
+  },
+  follow_requests: {
+    id: 'navigation_bar.follow_requests',
+    defaultMessage: 'Follow requests',
+  },
+  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
+  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
+  followed_tags: {
+    id: 'navigation_bar.followed_tags',
+    defaultMessage: 'Followed hashtags',
+  },
+  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
+  domain_blocks: {
+    id: 'navigation_bar.domain_blocks',
+    defaultMessage: 'Blocked domains',
+  },
+  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
+  endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
+  unendorse: {
+    id: 'account.unendorse',
+    defaultMessage: "Don't feature on profile",
+  },
+  add_or_remove_from_list: {
+    id: 'account.add_or_remove_from_list',
+    defaultMessage: 'Add or Remove from lists',
+  },
+  add_or_remove_from_antenna: {
+    id: 'account.add_or_remove_from_antenna',
+    defaultMessage: 'Add or Remove from antennas',
+  },
+  add_or_remove_from_exclude_antenna: {
+    id: 'account.add_or_remove_from_exclude_antenna',
+    defaultMessage: 'Add or Remove from antennas as exclusion',
+  },
+  add_or_remove_from_circle: {
+    id: 'account.add_or_remove_from_circle',
+    defaultMessage: 'Add or Remove from circles',
+  },
+  admin_account: {
+    id: 'status.admin_account',
+    defaultMessage: 'Open moderation interface for @{name}',
+  },
+  admin_domain: {
+    id: 'status.admin_domain',
+    defaultMessage: 'Open moderation interface for {domain}',
+  },
+  languages: {
+    id: 'account.languages',
+    defaultMessage: 'Change subscribed languages',
+  },
+  openOriginalPage: {
+    id: 'account.open_original_page',
+    defaultMessage: 'Open original page',
+  },
+});
+
+const titleFromAccount = (account: Account) => {
+  const displayName = account.display_name;
+  const acct =
+    account.acct === account.username
+      ? `${account.username}@${localDomain}`
+      : account.acct;
+  const prefix =
+    displayName.trim().length === 0 ? account.username : displayName;
+
+  return `${prefix} (@${acct})`;
+};
+
+const dateFormatOptions: Intl.DateTimeFormatOptions = {
+  month: 'short',
+  day: 'numeric',
+  year: 'numeric',
+  hour: '2-digit',
+  minute: '2-digit',
+};
+
+export const AccountHeader: React.FC<{
+  accountId: string;
+  hideTabs?: boolean;
+}> = ({ accountId, hideTabs }) => {
+  const dispatch = useAppDispatch();
+
+  const intl = useIntl();
+  const { signedIn, permissions } = useIdentity();
+  const account = useAppSelector((state) => state.accounts.get(accountId));
+  const relationship = useAppSelector((state) =>
+    state.relationships.get(accountId),
+  );
+  const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
+  const featuredTags = useAppSelector(
+    (state) =>
+      state.user_lists.getIn(['featured_tags', accountId, 'items']) as any,
+  );
+  const handleLinkClick = useLinks();
+
+  const handleBlock = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    if (relationship?.blocking) {
+      dispatch(unblockAccount(account.id));
+    } else {
+      dispatch(initBlockModal(account));
+    }
+  }, [dispatch, account, relationship]);
+
+  const handleMention = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(mentionCompose(account));
+  }, [dispatch, account]);
+
+  const handleDirect = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(directCompose(account));
+  }, [dispatch, account]);
+
+  const handleReport = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(initReport(account));
+  }, [dispatch, account]);
+
+  const handleReblogToggle = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    if (relationship?.showing_reblogs) {
+      dispatch(followAccount(account.id, { reblogs: false }));
+    } else {
+      dispatch(followAccount(account.id, { reblogs: true }));
+    }
+  }, [dispatch, account, relationship]);
+
+  const handleNotifyToggle = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    if (relationship?.notifying) {
+      dispatch(followAccount(account.id, { notify: false }));
+    } else {
+      dispatch(followAccount(account.id, { notify: true }));
+    }
+  }, [dispatch, account, relationship]);
+
+  const handleMute = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    if (relationship?.muting) {
+      dispatch(unmuteAccount(account.id));
+    } else {
+      dispatch(initMuteModal(account));
+    }
+  }, [dispatch, account, relationship]);
+
+  const handleBlockDomain = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(initDomainBlockModal(account));
+  }, [dispatch, account]);
+
+  const handleUnblockDomain = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    const domain = account.acct.split('@')[1];
+
+    if (!domain) {
+      return;
+    }
+
+    dispatch(unblockDomain(domain));
+  }, [dispatch, account]);
+
+  const handleEndorseToggle = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    if (relationship?.endorsed) {
+      dispatch(unpinAccount(account.id));
+    } else {
+      dispatch(pinAccount(account.id));
+    }
+  }, [dispatch, account, relationship]);
+
+  const handleAddToList = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(
+      openModal({
+        modalType: 'LIST_ADDER',
+        modalProps: {
+          accountId: account.id,
+        },
+      }),
+    );
+  }, [dispatch, account]);
+
+  const handleAddToAntenna = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(
+      openModal({
+        modalType: 'ANTENNA_ADDER',
+        modalProps: {
+          accountId: account.id,
+          isExclude: false,
+        },
+      }),
+    );
+  }, [dispatch, account]);
+
+  const handleAddToExcludeAntenna = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(
+      openModal({
+        modalType: 'ANTENNA_ADDER',
+        modalProps: {
+          accountId: account.id,
+          isExclude: true,
+        },
+      }),
+    );
+  }, [dispatch, account]);
+
+  const handleAddToCircle = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(
+      openModal({
+        modalType: 'CIRCLE_ADDER',
+        modalProps: {
+          accountId: account.id,
+        },
+      }),
+    );
+  }, [dispatch, account]);
+
+  const handleChangeLanguages = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    dispatch(
+      openModal({
+        modalType: 'SUBSCRIBED_LANGUAGES',
+        modalProps: {
+          accountId: account.id,
+        },
+      }),
+    );
+  }, [dispatch, account]);
+
+  const handleOpenAvatar = useCallback(
+    (e: React.MouseEvent) => {
+      if (e.button !== 0 || e.ctrlKey || e.metaKey) {
+        return;
+      }
+
+      e.preventDefault();
+
+      if (!account) {
+        return;
+      }
+
+      dispatch(
+        openModal({
+          modalType: 'IMAGE',
+          modalProps: {
+            src: account.avatar,
+            alt: '',
+          },
+        }),
+      );
+    },
+    [dispatch, account],
+  );
+
+  const handleShare = useCallback(() => {
+    if (!account) {
+      return;
+    }
+
+    void navigator.share({
+      url: account.url,
+    });
+  }, [account]);
+
+  const handleMouseEnter = useCallback(
+    ({ currentTarget }: React.MouseEvent) => {
+      if (autoPlayGif) {
+        return;
+      }
+
+      currentTarget
+        .querySelectorAll<HTMLImageElement>('.custom-emoji')
+        .forEach((emoji) => {
+          emoji.src = emoji.getAttribute('data-original') ?? '';
+        });
+    },
+    [],
+  );
+
+  const handleMouseLeave = useCallback(
+    ({ currentTarget }: React.MouseEvent) => {
+      if (autoPlayGif) {
+        return;
+      }
+
+      currentTarget
+        .querySelectorAll<HTMLImageElement>('.custom-emoji')
+        .forEach((emoji) => {
+          emoji.src = emoji.getAttribute('data-static') ?? '';
+        });
+    },
+    [],
+  );
+
+  const suspended = account?.suspended;
+  const isRemote = account?.acct !== account?.username;
+  const remoteDomain = isRemote ? account?.acct.split('@')[1] : null;
+
+  const menu = useMemo(() => {
+    const arr: MenuItem[] = [];
+
+    if (!account) {
+      return arr;
+    }
+
+    if (signedIn && account.id !== me && !account.suspended) {
+      arr.push({
+        text: intl.formatMessage(messages.mention, {
+          name: account.username,
+        }),
+        action: handleMention,
+      });
+      arr.push({
+        text: intl.formatMessage(messages.direct, {
+          name: account.username,
+        }),
+        action: handleDirect,
+      });
+      arr.push(null);
+    }
+
+    if (isRemote) {
+      arr.push({
+        text: intl.formatMessage(messages.openOriginalPage),
+        href: account.url,
+      });
+      arr.push(null);
+    }
+
+    if (account.id === me) {
+      arr.push({
+        text: intl.formatMessage(messages.edit_profile),
+        href: '/settings/profile',
+      });
+      arr.push({
+        text: intl.formatMessage(messages.preferences),
+        href: '/settings/preferences',
+      });
+      arr.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
+      arr.push(null);
+      arr.push({
+        text: intl.formatMessage(messages.follow_requests),
+        to: '/follow_requests',
+      });
+      arr.push({
+        text: intl.formatMessage(messages.favourites),
+        to: '/favourites',
+      });
+      arr.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
+      arr.push({
+        text: intl.formatMessage(messages.followed_tags),
+        to: '/followed_tags',
+      });
+      arr.push(null);
+      arr.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
+      arr.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
+      arr.push({
+        text: intl.formatMessage(messages.domain_blocks),
+        to: '/domain_blocks',
+      });
+    } else if (signedIn) {
+      if (relationship?.following) {
+        if (!relationship.muting) {
+          if (relationship.showing_reblogs) {
+            arr.push({
+              text: intl.formatMessage(messages.hideReblogs, {
+                name: account.username,
+              }),
+              action: handleReblogToggle,
+            });
+          } else {
+            arr.push({
+              text: intl.formatMessage(messages.showReblogs, {
+                name: account.username,
+              }),
+              action: handleReblogToggle,
+            });
+          }
+
+          arr.push({
+            text: intl.formatMessage(messages.languages),
+            action: handleChangeLanguages,
+          });
+          arr.push(null);
+        }
+
+        arr.push({
+          text: intl.formatMessage(
+            relationship.endorsed ? messages.unendorse : messages.endorse,
+          ),
+          action: handleEndorseToggle,
+        });
+        arr.push({
+          text: intl.formatMessage(messages.add_or_remove_from_list),
+          action: handleAddToList,
+        });
+      }
+      arr.push({
+        text: intl.formatMessage(messages.add_or_remove_from_antenna),
+        action: handleAddToAntenna,
+      });
+      arr.push({
+        text: intl.formatMessage(messages.add_or_remove_from_exclude_antenna),
+        action: handleAddToExcludeAntenna,
+      });
+      if (relationship?.followed_by) {
+        arr.push({
+          text: intl.formatMessage(messages.add_or_remove_from_circle),
+          action: handleAddToCircle,
+        });
+      }
+      arr.push(null);
+
+      if (relationship?.muting) {
+        arr.push({
+          text: intl.formatMessage(messages.unmute, {
+            name: account.username,
+          }),
+          action: handleMute,
+        });
+      } else {
+        arr.push({
+          text: intl.formatMessage(messages.mute, {
+            name: account.username,
+          }),
+          action: handleMute,
+          dangerous: true,
+        });
+      }
+
+      if (relationship?.blocking) {
+        arr.push({
+          text: intl.formatMessage(messages.unblock, {
+            name: account.username,
+          }),
+          action: handleBlock,
+        });
+      } else {
+        arr.push({
+          text: intl.formatMessage(messages.block, {
+            name: account.username,
+          }),
+          action: handleBlock,
+          dangerous: true,
+        });
+      }
+
+      if (!account.suspended) {
+        arr.push({
+          text: intl.formatMessage(messages.report, {
+            name: account.username,
+          }),
+          action: handleReport,
+          dangerous: true,
+        });
+      }
+    }
+
+    if (signedIn && isRemote) {
+      arr.push(null);
+
+      if (relationship?.domain_blocking) {
+        arr.push({
+          text: intl.formatMessage(messages.unblockDomain, {
+            domain: remoteDomain,
+          }),
+          action: handleUnblockDomain,
+        });
+      } else {
+        arr.push({
+          text: intl.formatMessage(messages.blockDomain, {
+            domain: remoteDomain,
+          }),
+          action: handleBlockDomain,
+          dangerous: true,
+        });
+      }
+    }
+
+    if (
+      (account.id !== me &&
+        (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) ||
+      (isRemote &&
+        (permissions & PERMISSION_MANAGE_FEDERATION) ===
+          PERMISSION_MANAGE_FEDERATION)
+    ) {
+      arr.push(null);
+      if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
+        arr.push({
+          text: intl.formatMessage(messages.admin_account, {
+            name: account.username,
+          }),
+          href: `/admin/accounts/${account.id}`,
+        });
+      }
+      if (
+        isRemote &&
+        (permissions & PERMISSION_MANAGE_FEDERATION) ===
+          PERMISSION_MANAGE_FEDERATION
+      ) {
+        arr.push({
+          text: intl.formatMessage(messages.admin_domain, {
+            domain: remoteDomain,
+          }),
+          href: `/admin/instances/${remoteDomain}`,
+        });
+      }
+    }
+
+    return arr;
+  }, [
+    account,
+    relationship,
+    permissions,
+    isRemote,
+    remoteDomain,
+    intl,
+    signedIn,
+    handleAddToAntenna,
+    handleAddToExcludeAntenna,
+    handleAddToCircle,
+    handleAddToList,
+    handleBlock,
+    handleBlockDomain,
+    handleChangeLanguages,
+    handleDirect,
+    handleEndorseToggle,
+    handleMention,
+    handleMute,
+    handleReblogToggle,
+    handleReport,
+    handleUnblockDomain,
+  ]);
+
+  if (!account) {
+    return null;
+  }
+
+  let actionBtn: React.ReactNode,
+    bellBtn: React.ReactNode,
+    lockedIcon: React.ReactNode,
+    shareBtn: React.ReactNode;
+
+  const info: React.ReactNode[] = [];
+
+  if (me !== account.id && relationship?.blocking) {
+    info.push(
+      <span key='blocked' className='relationship-tag'>
+        <FormattedMessage id='account.blocked' defaultMessage='Blocked' />
+      </span>,
+    );
+  }
+
+  if (me !== account.id && relationship?.muting) {
+    info.push(
+      <span key='muted' className='relationship-tag'>
+        <FormattedMessage id='account.muted' defaultMessage='Muted' />
+      </span>,
+    );
+  } else if (me !== account.id && relationship?.domain_blocking) {
+    info.push(
+      <span key='domain_blocked' className='relationship-tag'>
+        <FormattedMessage
+          id='account.domain_blocked'
+          defaultMessage='Domain blocked'
+        />
+      </span>,
+    );
+  }
+
+  if (relationship?.requested || relationship?.following) {
+    bellBtn = (
+      <IconButton
+        icon={relationship.notifying ? 'bell' : 'bell-o'}
+        iconComponent={
+          relationship.notifying ? NotificationsActiveIcon : NotificationsIcon
+        }
+        active={relationship.notifying}
+        title={intl.formatMessage(
+          relationship.notifying
+            ? messages.disableNotifications
+            : messages.enableNotifications,
+          { name: account.username },
+        )}
+        onClick={handleNotifyToggle}
+      />
+    );
+  }
+
+  if ('share' in navigator) {
+    shareBtn = (
+      <IconButton
+        className='optional'
+        icon=''
+        iconComponent={ShareIcon}
+        title={intl.formatMessage(messages.share, {
+          name: account.username,
+        })}
+        onClick={handleShare}
+      />
+    );
+  } else {
+    shareBtn = (
+      <CopyIconButton
+        className='optional'
+        title={intl.formatMessage(messages.copy)}
+        value={account.url}
+      />
+    );
+  }
+
+  if (relationship?.blocking) {
+    actionBtn = (
+      <Button
+        text={intl.formatMessage(messages.unblock, {
+          name: account.username,
+        })}
+        onClick={handleBlock}
+      />
+    );
+  } else {
+    actionBtn = <FollowButton accountId={accountId} />;
+  }
+
+  if (account.moved && !relationship?.following) {
+    actionBtn = '';
+  }
+
+  if (account.locked) {
+    lockedIcon = (
+      <Icon
+        id='lock'
+        icon={LockIcon}
+        title={intl.formatMessage(messages.account_locked)}
+      />
+    );
+  }
+
+  const content = { __html: account.note_emojified };
+  const displayNameHtml = { __html: account.display_name_html };
+  const fields = account.fields;
+  const isLocal = !account.acct.includes('@');
+  const username = account.acct.split('@')[0];
+  const domain = isLocal ? localDomain : account.acct.split('@')[1];
+  const isIndexable = !account.noindex;
+  const featuredTagsArr =
+    featuredTags?.map((tag: any) => tag.get('name')).toArray() || [];
+  const featuredTagsBar = getFeaturedHashtagBar(
+    account.id,
+    account.acct,
+    featuredTagsArr,
+  );
+
+  const badges = [];
+
+  if (account.bot) {
+    badges.push(<AutomatedBadge key='bot-badge' />);
+  } else if (account.group) {
+    badges.push(<GroupBadge key='group-badge' />);
+  }
+
+  account.roles.forEach((role) => {
+    badges.push(
+      <Badge
+        key={`role-badge-${role.get('id')}`}
+        label={<span>{role.get('name')}</span>}
+        domain={domain}
+        roleId={role.get('id')}
+      />,
+    );
+  });
+
+  return (
+    <div className='account-timeline__header'>
+      {!hidden && account.memorial && <MemorialNote />}
+      {!hidden && account.moved && (
+        <MovedNote accountId={account.id} targetAccountId={account.moved} />
+      )}
+
+      <div
+        className={classNames('account__header', {
+          inactive: !!account.moved,
+        })}
+        onMouseEnter={handleMouseEnter}
+        onMouseLeave={handleMouseLeave}
+      >
+        {!(suspended || hidden || account.moved) &&
+          relationship?.requested_by && (
+            <FollowRequestNoteContainer account={account} />
+          )}
+
+        <div className='account__header__image'>
+          <div className='account__header__info'>{info}</div>
+
+          {!(suspended || hidden) && (
+            <img
+              src={autoPlayGif ? account.header : account.header_static}
+              alt=''
+              className='parallax'
+            />
+          )}
+        </div>
+
+        <div className='account__header__bar'>
+          <div className='account__header__tabs'>
+            <a
+              className='avatar'
+              href={account.avatar}
+              rel='noopener'
+              target='_blank'
+              onClick={handleOpenAvatar}
+            >
+              <Avatar
+                account={suspended || hidden ? undefined : account}
+                size={90}
+              />
+            </a>
+
+            <div className='account__header__tabs__buttons'>
+              {!hidden && bellBtn}
+              {!hidden && shareBtn}
+              <Dropdown
+                disabled={menu.length === 0}
+                items={menu}
+                icon='ellipsis-v'
+                iconComponent={MoreHorizIcon}
+              />
+              {!hidden && actionBtn}
+            </div>
+          </div>
+
+          <div className='account__header__tabs__name'>
+            <h1>
+              <span dangerouslySetInnerHTML={displayNameHtml} />
+              <small>
+                <span>
+                  @{username}
+                  <span className='invisible'>@{domain}</span>
+                </span>
+                <DomainPill
+                  username={username ?? ''}
+                  domain={domain ?? ''}
+                  isSelf={me === account.id}
+                />
+                {lockedIcon}
+              </small>
+            </h1>
+          </div>
+
+          {badges.length > 0 && (
+            <div className='account__header__badges'>{badges}</div>
+          )}
+
+          {!(suspended || hidden) && (
+            <div className='account__header__extra'>
+              <div
+                className='account__header__bio'
+                onClickCapture={handleLinkClick}
+              >
+                {account.id !== me && signedIn && (
+                  <AccountNoteContainer accountId={accountId} />
+                )}
+
+                {account.note.length > 0 && account.note !== '<p></p>' && (
+                  <div
+                    className='account__header__content translate'
+                    dangerouslySetInnerHTML={content}
+                  />
+                )}
+
+                {featuredTagsArr.length > 0 && (
+                  <div className='account__header__featured-tags'>
+                    {featuredTagsBar}
+                  </div>
+                )}
+
+                <div className='account__header__fields'>
+                  <dl>
+                    <dt>
+                      <FormattedMessage
+                        id='account.joined_short'
+                        defaultMessage='Joined'
+                      />
+                    </dt>
+                    <dd>
+                      <FormattedDateWrapper
+                        value={account.created_at}
+                        year='numeric'
+                        month='short'
+                        day='2-digit'
+                      />
+                    </dd>
+                  </dl>
+
+                  {fields.map((pair, i) => (
+                    <dl
+                      key={i}
+                      className={classNames({
+                        verified: pair.verified_at,
+                      })}
+                    >
+                      <dt
+                        dangerouslySetInnerHTML={{
+                          __html: pair.name_emojified,
+                        }}
+                        title={pair.name}
+                        className='translate'
+                      />
+
+                      <dd className='translate' title={pair.value_plain ?? ''}>
+                        {pair.verified_at && (
+                          <span
+                            title={intl.formatMessage(messages.linkVerifiedOn, {
+                              date: intl.formatDate(
+                                pair.verified_at,
+                                dateFormatOptions,
+                              ),
+                            })}
+                          >
+                            <Icon
+                              id='check'
+                              icon={CheckIcon}
+                              className='verified__mark'
+                            />
+                          </span>
+                        )}{' '}
+                        <span
+                          dangerouslySetInnerHTML={{
+                            __html: pair.value_emojified,
+                          }}
+                        />
+                      </dd>
+                    </dl>
+                  ))}
+                </div>
+              </div>
+
+              <div className='account__header__extra__links'>
+                <NavLink
+                  to={`/@${account.acct}`}
+                  title={intl.formatNumber(account.statuses_count)}
+                >
+                  <ShortNumber
+                    value={account.statuses_count}
+                    isHide={account.other_settings.hide_statuses_count}
+                    renderer={StatusesCounter}
+                  />
+                </NavLink>
+
+                <NavLink
+                  exact
+                  to={`/@${account.acct}/following`}
+                  title={intl.formatNumber(account.following_count)}
+                >
+                  <ShortNumber
+                    value={account.following_count}
+                    isHide={account.other_settings.hide_following_count}
+                    renderer={FollowingCounter}
+                  />
+                </NavLink>
+
+                <NavLink
+                  exact
+                  to={`/@${account.acct}/followers`}
+                  title={intl.formatNumber(account.followers_count)}
+                >
+                  <ShortNumber
+                    value={account.followers_count}
+                    isHide={account.other_settings.hide_followers_count}
+                    renderer={FollowersCounter}
+                  />
+                </NavLink>
+              </div>
+            </div>
+          )}
+        </div>
+      </div>
+
+      {!(hideTabs || hidden) && (
+        <div className='account__section-headline'>
+          <NavLink exact to={`/@${account.acct}/featured`}>
+            <FormattedMessage id='account.featured' defaultMessage='Featured' />
+          </NavLink>
+          <NavLink exact to={`/@${account.acct}`}>
+            <FormattedMessage id='account.posts' defaultMessage='Posts' />
+          </NavLink>
+          <NavLink exact to={`/@${account.acct}/with_replies`}>
+            <FormattedMessage
+              id='account.posts_with_replies'
+              defaultMessage='Posts and replies'
+            />
+          </NavLink>
+          <NavLink exact to={`/@${account.acct}/media`}>
+            <FormattedMessage id='account.media' defaultMessage='Media' />
+          </NavLink>
+        </div>
+      )}
+
+      <Helmet>
+        <title>{titleFromAccount(account)}</title>
+        <meta
+          name='robots'
+          content={isLocal && isIndexable ? 'all' : 'noindex'}
+        />
+        <link rel='canonical' href={account.url} />
+      </Helmet>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.jsx b/app/javascript/mastodon/features/account_timeline/components/header.jsx
deleted file mode 100644
index d496558788..0000000000
--- a/app/javascript/mastodon/features/account_timeline/components/header.jsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { FormattedMessage } from 'react-intl';
-
-import { NavLink } from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import InnerHeader from '../../account/components/header';
-
-import MemorialNote from './memorial_note';
-import MovedNote from './moved_note';
-
-class Header extends ImmutablePureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.record,
-    featuredTags: PropTypes.array,
-    onFollow: PropTypes.func.isRequired,
-    onBlock: PropTypes.func.isRequired,
-    onMention: PropTypes.func.isRequired,
-    onDirect: PropTypes.func.isRequired,
-    onReblogToggle: PropTypes.func.isRequired,
-    onReport: PropTypes.func.isRequired,
-    onMute: PropTypes.func.isRequired,
-    onBlockDomain: PropTypes.func.isRequired,
-    onUnblockDomain: PropTypes.func.isRequired,
-    onEndorseToggle: PropTypes.func.isRequired,
-    onAddToList: PropTypes.func.isRequired,
-    onAddToAntenna: PropTypes.func.isRequired,
-    onAddToExcludeAntenna: PropTypes.func.isRequired,
-    onAddToCircle: PropTypes.func.isRequired,
-    onChangeLanguages: PropTypes.func.isRequired,
-    onInteractionModal: PropTypes.func.isRequired,
-    onOpenAvatar: PropTypes.func.isRequired,
-    onOpenURL: PropTypes.func.isRequired,
-    hideTabs: PropTypes.bool,
-    domain: PropTypes.string.isRequired,
-    hidden: PropTypes.bool,
-  };
-
-  handleFollow = () => {
-    this.props.onFollow(this.props.account);
-  };
-
-  handleBlock = () => {
-    this.props.onBlock(this.props.account);
-  };
-
-  handleMention = () => {
-    this.props.onMention(this.props.account);
-  };
-
-  handleDirect = () => {
-    this.props.onDirect(this.props.account);
-  };
-
-  handleReport = () => {
-    this.props.onReport(this.props.account);
-  };
-
-  handleReblogToggle = () => {
-    this.props.onReblogToggle(this.props.account);
-  };
-
-  handleNotifyToggle = () => {
-    this.props.onNotifyToggle(this.props.account);
-  };
-
-  handleMute = () => {
-    this.props.onMute(this.props.account);
-  };
-
-  handleBlockDomain = () => {
-    this.props.onBlockDomain(this.props.account);
-  };
-
-  handleUnblockDomain = () => {
-    const domain = this.props.account.get('acct').split('@')[1];
-
-    if (!domain) return;
-
-    this.props.onUnblockDomain(domain);
-  };
-
-  handleEndorseToggle = () => {
-    this.props.onEndorseToggle(this.props.account);
-  };
-
-  handleAddToList = () => {
-    this.props.onAddToList(this.props.account);
-  };
-
-  handleAddToAntenna = () => {
-    this.props.onAddToAntenna(this.props.account);
-  };
-
-  handleAddToExcludeAntenna = () => {
-    this.props.onAddToExcludeAntenna(this.props.account);
-  };
-
-  handleAddToCircle = () => {
-    this.props.onAddToCircle(this.props.account);
-  };
-
-  handleEditAccountNote = () => {
-    this.props.onEditAccountNote(this.props.account);
-  };
-
-  handleChangeLanguages = () => {
-    this.props.onChangeLanguages(this.props.account);
-  };
-
-  handleInteractionModal = () => {
-    this.props.onInteractionModal(this.props.account);
-  };
-
-  handleOpenAvatar = () => {
-    this.props.onOpenAvatar(this.props.account);
-  };
-
-  render () {
-    const { account, featuredTags, hidden, hideTabs } = this.props;
-
-    if (account === null) {
-      return null;
-    }
-
-    return (
-      <div className='account-timeline__header'>
-        {(!hidden && account.get('memorial')) && <MemorialNote />}
-        {(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
-
-        <InnerHeader
-          account={account}
-          featuredTags={featuredTags}
-          onFollow={this.handleFollow}
-          onBlock={this.handleBlock}
-          onMention={this.handleMention}
-          onDirect={this.handleDirect}
-          onReblogToggle={this.handleReblogToggle}
-          onNotifyToggle={this.handleNotifyToggle}
-          onReport={this.handleReport}
-          onMute={this.handleMute}
-          onBlockDomain={this.handleBlockDomain}
-          onUnblockDomain={this.handleUnblockDomain}
-          onEndorseToggle={this.handleEndorseToggle}
-          onAddToList={this.handleAddToList}
-          onAddToAntenna={this.handleAddToAntenna}
-          onAddToExcludeAntenna={this.handleAddToExcludeAntenna}
-          onAddToCircle={this.handleAddToCircle}
-          onEditAccountNote={this.handleEditAccountNote}
-          onChangeLanguages={this.handleChangeLanguages}
-          onInteractionModal={this.handleInteractionModal}
-          onOpenAvatar={this.handleOpenAvatar}
-          onOpenURL={this.props.onOpenURL}
-          domain={this.props.domain}
-          hidden={hidden}
-        />
-
-        {!(hideTabs || hidden) && (
-          <div className='account__section-headline'>
-            <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
-            <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
-            <NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
-          </div>
-        )}
-      </div>
-    );
-  }
-
-}
-
-export default Header;
diff --git a/app/javascript/mastodon/features/account_timeline/components/memorial_note.jsx b/app/javascript/mastodon/features/account_timeline/components/memorial_note.tsx
similarity index 53%
rename from app/javascript/mastodon/features/account_timeline/components/memorial_note.jsx
rename to app/javascript/mastodon/features/account_timeline/components/memorial_note.tsx
index a04808f1ca..19e6f0ed22 100644
--- a/app/javascript/mastodon/features/account_timeline/components/memorial_note.jsx
+++ b/app/javascript/mastodon/features/account_timeline/components/memorial_note.tsx
@@ -1,11 +1,12 @@
 import { FormattedMessage } from 'react-intl';
 
-const MemorialNote = () => (
+export const MemorialNote: React.FC = () => (
   <div className='account-memorial-banner'>
     <div className='account-memorial-banner__message'>
-      <FormattedMessage id='account.in_memoriam' defaultMessage='In Memoriam.' />
+      <FormattedMessage
+        id='account.in_memoriam'
+        defaultMessage='In Memoriam.'
+      />
     </div>
   </div>
 );
-
-export default MemorialNote;
diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.jsx b/app/javascript/mastodon/features/account_timeline/components/moved_note.jsx
deleted file mode 100644
index 2c996ff769..0000000000
--- a/app/javascript/mastodon/features/account_timeline/components/moved_note.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { FormattedMessage } from 'react-intl';
-
-import { Link } from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import { AvatarOverlay } from '../../../components/avatar_overlay';
-import { DisplayName } from '../../../components/display_name';
-
-export default class MovedNote extends ImmutablePureComponent {
-
-  static propTypes = {
-    from: ImmutablePropTypes.map.isRequired,
-    to: ImmutablePropTypes.map.isRequired,
-  };
-
-  render () {
-    const { from, to } = this.props;
-
-    return (
-      <div className='moved-account-banner'>
-        <div className='moved-account-banner__message'>
-          <FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: from.get('display_name_html') }} /></bdi> }} />
-        </div>
-
-        <div className='moved-account-banner__action'>
-          <Link to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
-            <div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
-            <DisplayName account={to} />
-          </Link>
-
-          <Link to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Link>
-        </div>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.tsx b/app/javascript/mastodon/features/account_timeline/components/moved_note.tsx
new file mode 100644
index 0000000000..51dbb93c8b
--- /dev/null
+++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.tsx
@@ -0,0 +1,53 @@
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { AvatarOverlay } from 'mastodon/components/avatar_overlay';
+import { DisplayName } from 'mastodon/components/display_name';
+import { useAppSelector } from 'mastodon/store';
+
+export const MovedNote: React.FC<{
+  accountId: string;
+  targetAccountId: string;
+}> = ({ accountId, targetAccountId }) => {
+  const from = useAppSelector((state) => state.accounts.get(accountId));
+  const to = useAppSelector((state) => state.accounts.get(targetAccountId));
+
+  return (
+    <div className='moved-account-banner'>
+      <div className='moved-account-banner__message'>
+        <FormattedMessage
+          id='account.moved_to'
+          defaultMessage='{name} has indicated that their new account is now:'
+          values={{
+            name: (
+              <bdi>
+                <strong
+                  dangerouslySetInnerHTML={{
+                    __html: from?.display_name_html ?? '',
+                  }}
+                />
+              </bdi>
+            ),
+          }}
+        />
+      </div>
+
+      <div className='moved-account-banner__action'>
+        <Link to={`/@${to?.acct}`} className='detailed-status__display-name'>
+          <div className='detailed-status__display-avatar'>
+            <AvatarOverlay account={to} friend={from} />
+          </div>
+          <DisplayName account={to} />
+        </Link>
+
+        <Link to={`/@${to?.acct}`} className='button'>
+          <FormattedMessage
+            id='account.go_to_profile'
+            defaultMessage='Go to profile'
+          />
+        </Link>
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx
deleted file mode 100644
index 56ad23b62f..0000000000
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import { injectIntl } from 'react-intl';
-
-import { connect } from 'react-redux';
-
-import { openURL } from 'mastodon/actions/search';
-
-import {
-  followAccount,
-  unblockAccount,
-  unmuteAccount,
-  pinAccount,
-  unpinAccount,
-} from '../../../actions/accounts';
-import { initBlockModal } from '../../../actions/blocks';
-import {
-  mentionCompose,
-  directCompose,
-} from '../../../actions/compose';
-import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
-import { openModal } from '../../../actions/modal';
-import { initMuteModal } from '../../../actions/mutes';
-import { initReport } from '../../../actions/reports';
-import { makeGetAccount, getAccountHidden } from '../../../selectors';
-import Header from '../components/header';
-
-const makeMapStateToProps = () => {
-  const getAccount = makeGetAccount();
-
-  const mapStateToProps = (state, { accountId }) => ({
-    account: getAccount(state, accountId),
-    domain: state.getIn(['meta', 'domain']),
-    hidden: getAccountHidden(state, accountId),
-    featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items']),
-  });
-
-  return mapStateToProps;
-};
-
-const mapDispatchToProps = (dispatch) => ({
-
-  onFollow (account) {
-    if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
-      dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
-    } else {
-      dispatch(followAccount(account.get('id')));
-    }
-  },
-
-  onInteractionModal (account) {
-    dispatch(openModal({
-      modalType: 'INTERACTION',
-      modalProps: {
-        type: 'follow',
-        accountId: account.get('id'),
-        url: account.get('uri'),
-      },
-    }));
-  },
-
-  onBlock (account) {
-    if (account.getIn(['relationship', 'blocking'])) {
-      dispatch(unblockAccount(account.get('id')));
-    } else {
-      dispatch(initBlockModal(account));
-    }
-  },
-
-  onMention (account) {
-    dispatch(mentionCompose(account));
-  },
-
-  onDirect (account) {
-    dispatch(directCompose(account));
-  },
-
-  onReblogToggle (account) {
-    if (account.getIn(['relationship', 'showing_reblogs'])) {
-      dispatch(followAccount(account.get('id'), { reblogs: false }));
-    } else {
-      dispatch(followAccount(account.get('id'), { reblogs: true }));
-    }
-  },
-
-  onEndorseToggle (account) {
-    if (account.getIn(['relationship', 'endorsed'])) {
-      dispatch(unpinAccount(account.get('id')));
-    } else {
-      dispatch(pinAccount(account.get('id')));
-    }
-  },
-
-  onNotifyToggle (account) {
-    if (account.getIn(['relationship', 'notifying'])) {
-      dispatch(followAccount(account.get('id'), { notify: false }));
-    } else {
-      dispatch(followAccount(account.get('id'), { notify: true }));
-    }
-  },
-
-  onReport (account) {
-    dispatch(initReport(account));
-  },
-
-  onMute (account) {
-    if (account.getIn(['relationship', 'muting'])) {
-      dispatch(unmuteAccount(account.get('id')));
-    } else {
-      dispatch(initMuteModal(account));
-    }
-  },
-
-  onBlockDomain (account) {
-    dispatch(initDomainBlockModal(account));
-  },
-
-  onUnblockDomain (domain) {
-    dispatch(unblockDomain(domain));
-  },
-
-  onAddToList (account) {
-    dispatch(openModal({
-      modalType: 'LIST_ADDER',
-      modalProps: {
-        accountId: account.get('id'),
-      },
-    }));
-  },
-
-  onAddToAntenna (account) {
-    dispatch(openModal({
-      modalType: 'ANTENNA_ADDER',
-      modalProps: {
-        accountId: account.get('id'),
-        isExclude: false,
-      },
-    }));
-  },
-
-  onAddToExcludeAntenna (account) {
-    dispatch(openModal({
-      modalType: 'ANTENNA_ADDER',
-      modalProps: {
-        accountId: account.get('id'),
-        isExclude: true,
-      },
-    }));
-  },
-
-  onAddToCircle (account) {
-    dispatch(openModal({
-      modalType: 'CIRCLE_ADDER',
-      modalProps: {
-        accountId: account.get('id'),
-      },
-    }));
-  },
-
-  onChangeLanguages (account) {
-    dispatch(openModal({
-      modalType: 'SUBSCRIBED_LANGUAGES',
-      modalProps: {
-        accountId: account.get('id'),
-      },
-    }));
-  },
-
-  onOpenAvatar (account) {
-    dispatch(openModal({
-      modalType: 'IMAGE',
-      modalProps: {
-        src: account.get('avatar'),
-        alt: '',
-      },
-    }));
-  },
-
-  onOpenURL (url) {
-    return dispatch(openURL({ url }));
-  },
-
-});
-
-export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx
index 105c2e4e50..a5223275b3 100644
--- a/app/javascript/mastodon/features/account_timeline/index.jsx
+++ b/app/javascript/mastodon/features/account_timeline/index.jsx
@@ -7,12 +7,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
 
-import { TimelineHint } from 'mastodon/components/timeline_hint';
 import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 import { me } from 'mastodon/initial_state';
 import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
-import { getAccountHidden } from 'mastodon/selectors';
-import { useAppSelector } from 'mastodon/store';
+import { getAccountHidden } from 'mastodon/selectors/accounts';
 
 import { lookupAccount, fetchAccount } from '../../actions/accounts';
 import { fetchFeaturedTags } from '../../actions/featured_tags';
@@ -21,9 +19,10 @@ import { ColumnBackButton } from '../../components/column_back_button';
 import { LoadingIndicator } from '../../components/loading_indicator';
 import StatusList from '../../components/status_list';
 import Column from '../ui/components/column';
+import { RemoteHint } from 'mastodon/components/remote_hint';
 
+import { AccountHeader } from './components/account_header';
 import { LimitedAccountHint } from './components/limited_account_hint';
-import HeaderContainer from './containers/header_container';
 
 const emptyList = ImmutableList();
 
@@ -47,11 +46,8 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
 
   return {
     accountId,
-    remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
-    remoteUrl: state.getIn(['accounts', accountId, 'url']),
     isAccount: !!state.getIn(['accounts', accountId]),
     statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
-    featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], emptyList),
     isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
     hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
     suspended: state.getIn(['accounts', accountId, 'suspended'], false),
@@ -60,24 +56,6 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
   };
 };
 
-const RemoteHint = ({ accountId, url }) => {
-  const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
-  const domain = acct ? acct.split('@')[1] : undefined;
-
-  return (
-    <TimelineHint
-      url={url}
-      message={<FormattedMessage id='hints.profiles.posts_may_be_missing' defaultMessage='Some posts from this profile may be missing.' />}
-      label={<FormattedMessage id='hints.profiles.see_more_posts' defaultMessage='See more posts on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
-    />
-  );
-};
-
-RemoteHint.propTypes = {
-  url: PropTypes.string.isRequired,
-  accountId: PropTypes.string.isRequired,
-};
-
 class AccountTimeline extends ImmutablePureComponent {
 
   static propTypes = {
@@ -89,7 +67,6 @@ class AccountTimeline extends ImmutablePureComponent {
     accountId: PropTypes.string,
     dispatch: PropTypes.func.isRequired,
     statusIds: ImmutablePropTypes.list,
-    featuredStatusIds: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
     withReplies: PropTypes.bool,
@@ -97,8 +74,6 @@ class AccountTimeline extends ImmutablePureComponent {
     isAccount: PropTypes.bool,
     suspended: PropTypes.bool,
     hidden: PropTypes.bool,
-    remote: PropTypes.bool,
-    remoteUrl: PropTypes.string,
     multiColumn: PropTypes.bool,
   };
 
@@ -161,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
   };
 
   render () {
-    const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
+    const { accountId, statusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
 
     if (isLoading && statusIds.isEmpty()) {
       return (
@@ -191,19 +166,16 @@ class AccountTimeline extends ImmutablePureComponent {
       emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
     }
 
-    const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
-
     return (
       <Column>
         <ColumnBackButton />
 
         <StatusList
-          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
+          prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
           alwaysPrepend
-          append={remoteMessage}
+          append={<RemoteHint accountId={accountId} />}
           scrollKey='account_timeline'
           statusIds={forceEmptyState ? emptyList : statusIds}
-          featuredStatusIds={featuredStatusIds}
           isLoading={isLoading}
           hasMore={!forceEmptyState && hasMore}
           onLoadMore={this.handleLoadMore}
diff --git a/app/javascript/mastodon/features/alt_text_modal/components/info_button.tsx b/app/javascript/mastodon/features/alt_text_modal/components/info_button.tsx
index f867dfb393..aecf9cbc2f 100644
--- a/app/javascript/mastodon/features/alt_text_modal/components/info_button.tsx
+++ b/app/javascript/mastodon/features/alt_text_modal/components/info_button.tsx
@@ -6,9 +6,9 @@ import classNames from 'classnames';
 
 import Overlay from 'react-overlays/Overlay';
 
-import { useSelectableClick } from '@/hooks/useSelectableClick';
 import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
 import { Icon } from 'mastodon/components/icon';
+import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
 
 const messages = defineMessages({
   help: { id: 'info_button.label', defaultMessage: 'Help' },
diff --git a/app/javascript/mastodon/features/alt_text_modal/index.tsx b/app/javascript/mastodon/features/alt_text_modal/index.tsx
index 80c4f36105..e2d05a99ca 100644
--- a/app/javascript/mastodon/features/alt_text_modal/index.tsx
+++ b/app/javascript/mastodon/features/alt_text_modal/index.tsx
@@ -2,7 +2,6 @@ import {
   useState,
   useCallback,
   useRef,
-  useEffect,
   useImperativeHandle,
   forwardRef,
 } from 'react';
@@ -13,6 +12,7 @@ import classNames from 'classnames';
 
 import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
 
+import { useSpring, animated } from '@react-spring/web';
 import Textarea from 'react-textarea-autosize';
 import { length } from 'stringz';
 // eslint-disable-next-line import/extensions
@@ -30,8 +30,8 @@ import { Skeleton } from 'mastodon/components/skeleton';
 import Audio from 'mastodon/features/audio';
 import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
 import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
-import Video, { getPointerPosition } from 'mastodon/features/video';
-import { me } from 'mastodon/initial_state';
+import { Video, getPointerPosition } from 'mastodon/features/video';
+import { me, reduceMotion } from 'mastodon/initial_state';
 import type { MediaAttachment } from 'mastodon/models/media_attachment';
 import { useAppSelector, useAppDispatch } from 'mastodon/store';
 import { assetHost } from 'mastodon/utils/config';
@@ -105,6 +105,17 @@ const Preview: React.FC<{
   position: FocalPoint;
   onPositionChange: (arg0: FocalPoint) => void;
 }> = ({ mediaId, position, onPositionChange }) => {
+  const draggingRef = useRef<boolean>(false);
+  const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
+
+  const [x, y] = position;
+  const style = useSpring({
+    to: {
+      left: `${x * 100}%`,
+      top: `${y * 100}%`,
+    },
+    immediate: reduceMotion || draggingRef.current,
+  });
   const media = useAppSelector((state) =>
     (
       (state.compose as ImmutableMap<string, unknown>).get(
@@ -117,9 +128,6 @@ const Preview: React.FC<{
   );
 
   const [dragging, setDragging] = useState(false);
-  const [x, y] = position;
-  const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
-  const draggingRef = useRef<boolean>(false);
 
   const setRef = useCallback(
     (e: HTMLImageElement | HTMLVideoElement | null) => {
@@ -134,62 +142,30 @@ const Preview: React.FC<{
         return;
       }
 
-      const { x, y } = getPointerPosition(nodeRef.current, e);
+      const handleMouseMove = (e: MouseEvent) => {
+        const { x, y } = getPointerPosition(nodeRef.current, e);
+        draggingRef.current = true; // This will disable the animation for quicker feedback, only do this if the mouse actually moves
+        onPositionChange([x, y]);
+      };
+
+      const handleMouseUp = () => {
+        setDragging(false);
+        draggingRef.current = false;
+        document.removeEventListener('mouseup', handleMouseUp);
+        document.removeEventListener('mousemove', handleMouseMove);
+      };
+
+      const { x, y } = getPointerPosition(nodeRef.current, e.nativeEvent);
+
       setDragging(true);
-      draggingRef.current = true;
       onPositionChange([x, y]);
+
+      document.addEventListener('mouseup', handleMouseUp);
+      document.addEventListener('mousemove', handleMouseMove);
     },
     [setDragging, onPositionChange],
   );
 
-  const handleTouchStart = useCallback(
-    (e: React.TouchEvent) => {
-      const { x, y } = getPointerPosition(nodeRef.current, e);
-      setDragging(true);
-      draggingRef.current = true;
-      onPositionChange([x, y]);
-    },
-    [setDragging, onPositionChange],
-  );
-
-  useEffect(() => {
-    const handleMouseUp = () => {
-      setDragging(false);
-      draggingRef.current = false;
-    };
-
-    const handleMouseMove = (e: MouseEvent) => {
-      if (draggingRef.current) {
-        const { x, y } = getPointerPosition(nodeRef.current, e);
-        onPositionChange([x, y]);
-      }
-    };
-
-    const handleTouchEnd = () => {
-      setDragging(false);
-      draggingRef.current = false;
-    };
-
-    const handleTouchMove = (e: TouchEvent) => {
-      if (draggingRef.current) {
-        const { x, y } = getPointerPosition(nodeRef.current, e);
-        onPositionChange([x, y]);
-      }
-    };
-
-    document.addEventListener('mouseup', handleMouseUp);
-    document.addEventListener('mousemove', handleMouseMove);
-    document.addEventListener('touchend', handleTouchEnd);
-    document.addEventListener('touchmove', handleTouchMove);
-
-    return () => {
-      document.removeEventListener('mouseup', handleMouseUp);
-      document.removeEventListener('mousemove', handleMouseMove);
-      document.removeEventListener('touchend', handleTouchEnd);
-      document.removeEventListener('touchmove', handleTouchMove);
-    };
-  }, [setDragging, onPositionChange]);
-
   if (!media) {
     return null;
   }
@@ -204,12 +180,8 @@ const Preview: React.FC<{
           alt=''
           role='presentation'
           onMouseDown={handleMouseDown}
-          onTouchStart={handleTouchStart}
-        />
-        <div
-          className='focal-point__reticle'
-          style={{ top: `${y * 100}%`, left: `${x * 100}%` }}
         />
+        <animated.div className='focal-point__reticle' style={style} />
       </div>
     );
   } else if (media.get('type') === 'gifv') {
@@ -220,12 +192,8 @@ const Preview: React.FC<{
           src={media.get('url') as string}
           alt=''
           onMouseDown={handleMouseDown}
-          onTouchStart={handleTouchStart}
-        />
-        <div
-          className='focal-point__reticle'
-          style={{ top: `${y * 100}%`, left: `${x * 100}%` }}
         />
+        <animated.div className='focal-point__reticle' style={style} />
       </div>
     );
   } else if (media.get('type') === 'video') {
@@ -233,10 +201,10 @@ const Preview: React.FC<{
       <Video
         preview={media.get('preview_url') as string}
         frameRate={media.getIn(['meta', 'original', 'frame_rate']) as string}
+        aspectRatio={`${media.getIn(['meta', 'original', 'width']) as number} / ${media.getIn(['meta', 'original', 'height']) as number}`}
         blurhash={media.get('blurhash') as string}
         src={media.get('url') as string}
         detailed
-        inline
         editable
       />
     );
diff --git a/app/javascript/mastodon/features/antennas/index.tsx b/app/javascript/mastodon/features/antennas/index.tsx
index 93ad4b6f48..c411cee735 100644
--- a/app/javascript/mastodon/features/antennas/index.tsx
+++ b/app/javascript/mastodon/features/antennas/index.tsx
@@ -13,9 +13,9 @@ import { fetchAntennas } from 'mastodon/actions/antennas';
 import { openModal } from 'mastodon/actions/modal';
 import { Column } from 'mastodon/components/column';
 import { ColumnHeader } from 'mastodon/components/column_header';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { Icon } from 'mastodon/components/icon';
 import ScrollableList from 'mastodon/components/scrollable_list';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
 import { getOrderedAntennas } from 'mastodon/selectors/antennas';
 import { useAppSelector, useAppDispatch } from 'mastodon/store';
 
@@ -96,12 +96,11 @@ const AntennaItem: React.FC<{
         </span>
       </Link>
 
-      <DropdownMenuContainer
+      <Dropdown
         scrollKey='antennas'
         items={menu}
-        icons='ellipsis-h'
+        icon='ellipsis-h'
         iconComponent={MoreHorizIcon}
-        direction='right'
         title={intl.formatMessage(messages.more)}
       />
     </div>
diff --git a/app/javascript/mastodon/features/audio/index.jsx b/app/javascript/mastodon/features/audio/index.jsx
index 56c0e524ad..53ce3f0bdb 100644
--- a/app/javascript/mastodon/features/audio/index.jsx
+++ b/app/javascript/mastodon/features/audio/index.jsx
@@ -1,7 +1,7 @@
 import PropTypes from 'prop-types';
 import { PureComponent } from 'react';
 
-import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 
 import classNames from 'classnames';
 
@@ -16,6 +16,7 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac
 import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
 import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
 import { Icon }  from 'mastodon/components/icon';
+import { SpoilerButton } from 'mastodon/components/spoiler_button';
 import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/features/video';
 
 import { Blurhash } from '../../components/blurhash';
@@ -26,8 +27,8 @@ import Visualizer from './visualizer';
 const messages = defineMessages({
   play: { id: 'video.play', defaultMessage: 'Play' },
   pause: { id: 'video.pause', defaultMessage: 'Pause' },
-  mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
-  unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
+  mute: { id: 'video.mute', defaultMessage: 'Mute' },
+  unmute: { id: 'video.unmute', defaultMessage: 'Unmute' },
   download: { id: 'video.download', defaultMessage: 'Download file' },
   hide: { id: 'audio.hide', defaultMessage: 'Hide audio' },
 });
@@ -61,6 +62,7 @@ class Audio extends PureComponent {
     volume: PropTypes.number,
     muted: PropTypes.bool,
     deployPictureInPicture: PropTypes.func,
+    matchedFilters: PropTypes.arrayOf(PropTypes.string),
   };
 
   state = {
@@ -471,19 +473,11 @@ class Audio extends PureComponent {
   };
 
   render () {
-    const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
+    const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash, matchedFilters } = this.props;
     const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
     const progress = Math.min((currentTime / duration) * 100, 100);
     const muted = this.state.muted || volume === 0;
 
-    let warning;
-
-    if (sensitive) {
-      warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
-    } else {
-      warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
-    }
-
     return (
       <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
 
@@ -521,14 +515,7 @@ class Audio extends PureComponent {
           lang={lang}
         />
 
-        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
-          <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
-            <span className='spoiler-button__overlay__label'>
-              {warning}
-              <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
-            </span>
-          </button>
-        </div>
+        <SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
 
         {(revealed || editable) && <img
           src={this.props.poster}
diff --git a/app/javascript/mastodon/features/bookmark_categories/index.tsx b/app/javascript/mastodon/features/bookmark_categories/index.tsx
index 68019b1921..92a2647989 100644
--- a/app/javascript/mastodon/features/bookmark_categories/index.tsx
+++ b/app/javascript/mastodon/features/bookmark_categories/index.tsx
@@ -14,9 +14,9 @@ import { fetchBookmarkCategories } from 'mastodon/actions/bookmark_categories';
 import { openModal } from 'mastodon/actions/modal';
 import { Column } from 'mastodon/components/column';
 import { ColumnHeader } from 'mastodon/components/column_header';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { Icon } from 'mastodon/components/icon';
 import ScrollableList from 'mastodon/components/scrollable_list';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
 import { getOrderedBookmarkCategories } from 'mastodon/selectors/bookmark_categories';
 import { useAppSelector, useAppDispatch } from 'mastodon/store';
 
@@ -76,12 +76,11 @@ const BookmarkCategoryItem: React.FC<{
         <span>{title}</span>
       </Link>
 
-      <DropdownMenuContainer
+      <Dropdown
         scrollKey='bookmark_categories'
         items={menu}
-        icons='ellipsis-h'
+        icon='ellipsis-h'
         iconComponent={MoreHorizIcon}
-        direction='right'
         title={intl.formatMessage(messages.more)}
       />
     </div>
diff --git a/app/javascript/mastodon/features/bookmark_category_statuses/index.jsx b/app/javascript/mastodon/features/bookmark_category_statuses/index.jsx
deleted file mode 100644
index 6a3feb46a4..0000000000
--- a/app/javascript/mastodon/features/bookmark_category_statuses/index.jsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-
-import { Helmet } from 'react-helmet';
-import { withRouter } from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-
-import { debounce } from 'lodash';
-
-import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg';
-import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
-import EditIcon from '@/material-icons/400-24px/edit.svg?react';
-import { deleteBookmarkCategory, expandBookmarkCategoryStatuses, fetchBookmarkCategory, fetchBookmarkCategoryStatuses } from 'mastodon/actions/bookmark_categories';
-import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
-import { openModal } from 'mastodon/actions/modal';
-import Column from 'mastodon/components/column';
-import ColumnHeader from 'mastodon/components/column_header';
-import { Icon }  from 'mastodon/components/icon';
-import { LoadingIndicator } from 'mastodon/components/loading_indicator';
-import StatusList from 'mastodon/components/status_list';
-import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
-import { getBookmarkCategoryStatusList } from 'mastodon/selectors';
-import { WithRouterPropTypes } from 'mastodon/utils/react_router';
-
-
-const messages = defineMessages({
-  deleteMessage: { id: 'confirmations.delete_bookmark_category.message', defaultMessage: 'Are you sure you want to permanently delete this category?' },
-  deleteConfirm: { id: 'confirmations.delete_bookmark_category.confirm', defaultMessage: 'Delete' },
-  heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
-});
-
-const mapStateToProps = (state, { params }) => ({
-  bookmarkCategory: state.getIn(['bookmark_categories', params.id]),
-  statusIds: getBookmarkCategoryStatusList(state, params.id),
-  isLoading: state.getIn(['status_lists', 'bookmark_category_statuses', params.id, 'isLoading'], true),
-  hasMore: !!state.getIn(['status_lists', 'bookmark_category_statuses', params.id, 'next']),
-});
-
-class BookmarkCategoryStatuses extends ImmutablePureComponent {
-
-  static propTypes = {
-    params: PropTypes.object.isRequired,
-    dispatch: PropTypes.func.isRequired,
-    statusIds: ImmutablePropTypes.list.isRequired,
-    bookmarkCategory: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
-    intl: PropTypes.object.isRequired,
-    columnId: PropTypes.string,
-    multiColumn: PropTypes.bool,
-    hasMore: PropTypes.bool,
-    isLoading: PropTypes.bool,
-    ...WithRouterPropTypes,
-  };
-
-  UNSAFE_componentWillMount () {
-    this.props.dispatch(fetchBookmarkCategory(this.props.params.id));
-    this.props.dispatch(fetchBookmarkCategoryStatuses(this.props.params.id));
-  }
-
-  UNSAFE_componentWillReceiveProps (nextProps) {
-    const { dispatch } = this.props;
-    const { id } = nextProps.params;
-
-    if (id !== this.props.params.id) {
-      dispatch(fetchBookmarkCategory(id));
-      dispatch(fetchBookmarkCategoryStatuses(id));
-    }
-  }
-
-  handlePin = () => {
-    const { columnId, dispatch } = this.props;
-
-    if (columnId) {
-      dispatch(removeColumn(columnId));
-    } else {
-      dispatch(addColumn('BOOKMARKS_EX', { id: this.props.params.id }));
-      this.props.history.push('/');
-    }
-  };
-
-  handleMove = (dir) => {
-    const { columnId, dispatch } = this.props;
-    dispatch(moveColumn(columnId, dir));
-  };
-
-  handleHeaderClick = () => {
-    this.column.scrollTop();
-  };
-
-  handleEditClick = () => {
-    this.props.history.push(`/bookmark_categories/${this.props.params.id}/edit`);
-  };
-
-  handleDeleteClick = () => {
-    const { dispatch, columnId, intl } = this.props;
-    const { id } = this.props.params;
-
-    dispatch(openModal({
-      modalType: 'CONFIRM',
-      modalProps: {
-        message: intl.formatMessage(messages.deleteMessage),
-        confirm: intl.formatMessage(messages.deleteConfirm),
-        onConfirm: () => {
-          dispatch(deleteBookmarkCategory(id));
-
-          if (columnId) {
-            dispatch(removeColumn(columnId));
-          } else {
-            this.props.history.push('/bookmark_categories');
-          }
-        },
-      },
-    }));
-  };
-
-  setRef = c => {
-    this.column = c;
-  };
-
-  handleLoadMore = debounce(() => {
-    this.props.dispatch(expandBookmarkCategoryStatuses(this.props.params.id));
-  }, 300, { leading: true });
-
-  render () {
-    const { intl, bookmarkCategory, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
-    const pinned = !!columnId;
-
-    if (typeof bookmarkCategory === 'undefined') {
-      return (
-        <Column>
-          <div className='scrollable'>
-            <LoadingIndicator />
-          </div>
-        </Column>
-      );
-    } else if (bookmarkCategory === false) {
-      return (
-        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
-      );
-    }
-
-    const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here." />;
-
-    return (
-      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
-        <ColumnHeader
-          icon='bookmark'
-          iconComponent={BookmarkIcon}
-          title={bookmarkCategory.get('title')}
-          onPin={this.handlePin}
-          onMove={this.handleMove}
-          onClick={this.handleHeaderClick}
-          pinned={pinned}
-          multiColumn={multiColumn}
-        >
-          <div className='column-settings'>
-            <section className='column-header__links'>
-              <button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
-                <Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='bookmark_categories.edit' defaultMessage='Edit category' />
-              </button>
-
-              <button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
-                <Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='bookmark_categories.delete' defaultMessage='Delete category' />
-              </button>
-            </section>
-          </div>
-        </ColumnHeader>
-
-        <StatusList
-          trackScroll={!pinned}
-          statusIds={statusIds}
-          scrollKey={`bookmark_ex_statuses-${columnId}`}
-          hasMore={hasMore}
-          isLoading={isLoading}
-          onLoadMore={this.handleLoadMore}
-          emptyMessage={emptyMessage}
-          bindToDocument={!multiColumn}
-        />
-
-        <Helmet>
-          <title>{intl.formatMessage(messages.heading)}</title>
-          <meta name='robots' content='noindex' />
-        </Helmet>
-      </Column>
-    );
-  }
-
-}
-
-export default withRouter(connect(mapStateToProps)(injectIntl(BookmarkCategoryStatuses)));
diff --git a/app/javascript/mastodon/features/bookmark_category_statuses/index.tsx b/app/javascript/mastodon/features/bookmark_category_statuses/index.tsx
new file mode 100644
index 0000000000..7facdcc449
--- /dev/null
+++ b/app/javascript/mastodon/features/bookmark_category_statuses/index.tsx
@@ -0,0 +1,121 @@
+import { useEffect, useRef, useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+import { useParams } from 'react-router';
+
+import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
+import {
+  expandBookmarkCategoryStatuses,
+  fetchBookmarkCategory,
+  fetchBookmarkCategoryStatuses,
+} from 'mastodon/actions/bookmark_categories';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusList from 'mastodon/components/status_list';
+import { getSubStatusList } from 'mastodon/selectors';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const BookmarkCategoryStatuses: React.FC<{
+  columnId: string;
+  multiColumn: boolean;
+}> = ({ columnId, multiColumn }) => {
+  const dispatch = useAppDispatch();
+  const { id } = useParams<{ id: string }>();
+  const columnRef = useRef<ColumnRef>(null);
+  const statusIds = useAppSelector((state) =>
+    getSubStatusList(state, 'bookmark_category', id),
+  );
+  const isLoading = useAppSelector(
+    (state) =>
+      state.status_lists.getIn(
+        ['bookmark_category_statuses', id, 'isLoading'],
+        true,
+      ) as boolean,
+  );
+  const hasMore = useAppSelector(
+    (state) =>
+      !!state.status_lists.getIn(['bookmark_category_statuses', id, 'next']),
+  );
+  const bookmarkCategory = useAppSelector((state) =>
+    state.bookmark_categories.get(id),
+  );
+
+  useEffect(() => {
+    dispatch(fetchBookmarkCategory(id));
+    dispatch(fetchBookmarkCategoryStatuses(id));
+  }, [dispatch, id]);
+
+  const handlePin = useCallback(() => {
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('BOOKMARKS_EX', { id }));
+    }
+  }, [dispatch, columnId, id]);
+
+  const handleMove = useCallback(
+    (dir: number) => {
+      dispatch(moveColumn(columnId, dir));
+    },
+    [dispatch, columnId],
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const handleLoadMore = useCallback(() => {
+    dispatch(expandBookmarkCategoryStatuses(id));
+  }, [dispatch, id]);
+
+  const pinned = !!columnId;
+
+  const emptyMessage = (
+    <FormattedMessage
+      id='empty_column.bookmarked_statuses'
+      defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here."
+    />
+  );
+
+  return (
+    <Column
+      bindToDocument={!multiColumn}
+      ref={columnRef}
+      label={bookmarkCategory?.get('title')}
+    >
+      <ColumnHeader
+        icon='bookmark'
+        iconComponent={BookmarkIcon}
+        title={bookmarkCategory?.get('title')}
+        onPin={handlePin}
+        onMove={handleMove}
+        onClick={handleHeaderClick}
+        pinned={pinned}
+        multiColumn={multiColumn}
+      />
+
+      <StatusList
+        trackScroll={!pinned}
+        statusIds={statusIds}
+        scrollKey={`bookmark_ex_statuses-${columnId}`}
+        hasMore={hasMore}
+        isLoading={isLoading}
+        onLoadMore={handleLoadMore}
+        emptyMessage={emptyMessage}
+        bindToDocument={!multiColumn}
+      />
+
+      <Helmet>
+        <title>{bookmarkCategory?.get('title')}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default BookmarkCategoryStatuses;
diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx
deleted file mode 100644
index 93be1c6b2e..0000000000
--- a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx
+++ /dev/null
@@ -1,116 +0,0 @@
-// Kmyblue tracking marker: copied bookmark_category_statuses, circle_statuses
-
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import { Helmet } from 'react-helmet';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { debounce } from 'lodash';
-
-import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
-import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'mastodon/actions/bookmarks';
-import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
-import Column from 'mastodon/components/column';
-import ColumnHeader from 'mastodon/components/column_header';
-import StatusList from 'mastodon/components/status_list';
-import { getStatusList } from 'mastodon/selectors';
-
-const messages = defineMessages({
-  heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
-});
-
-const mapStateToProps = state => ({
-  statusIds: getStatusList(state, 'bookmarks'),
-  isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
-  hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
-});
-
-class Bookmarks extends ImmutablePureComponent {
-
-  static propTypes = {
-    dispatch: PropTypes.func.isRequired,
-    statusIds: ImmutablePropTypes.list.isRequired,
-    intl: PropTypes.object.isRequired,
-    columnId: PropTypes.string,
-    multiColumn: PropTypes.bool,
-    hasMore: PropTypes.bool,
-    isLoading: PropTypes.bool,
-  };
-
-  UNSAFE_componentWillMount () {
-    this.props.dispatch(fetchBookmarkedStatuses());
-  }
-
-  handlePin = () => {
-    const { columnId, dispatch } = this.props;
-
-    if (columnId) {
-      dispatch(removeColumn(columnId));
-    } else {
-      dispatch(addColumn('BOOKMARKS', {}));
-    }
-  };
-
-  handleMove = (dir) => {
-    const { columnId, dispatch } = this.props;
-    dispatch(moveColumn(columnId, dir));
-  };
-
-  handleHeaderClick = () => {
-    this.column.scrollTop();
-  };
-
-  setRef = c => {
-    this.column = c;
-  };
-
-  handleLoadMore = debounce(() => {
-    this.props.dispatch(expandBookmarkedStatuses());
-  }, 300, { leading: true });
-
-  render () {
-    const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
-    const pinned = !!columnId;
-
-    const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here." />;
-
-    return (
-      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
-        <ColumnHeader
-          icon='bookmarks'
-          iconComponent={BookmarksIcon}
-          title={intl.formatMessage(messages.heading)}
-          onPin={this.handlePin}
-          onMove={this.handleMove}
-          onClick={this.handleHeaderClick}
-          pinned={pinned}
-          multiColumn={multiColumn}
-        />
-
-        <StatusList
-          trackScroll={!pinned}
-          statusIds={statusIds}
-          scrollKey={`bookmarked_statuses-${columnId}`}
-          hasMore={hasMore}
-          isLoading={isLoading}
-          onLoadMore={this.handleLoadMore}
-          emptyMessage={emptyMessage}
-          bindToDocument={!multiColumn}
-        />
-
-        <Helmet>
-          <title>{intl.formatMessage(messages.heading)}</title>
-          <meta name='robots' content='noindex' />
-        </Helmet>
-      </Column>
-    );
-  }
-
-}
-
-export default connect(mapStateToProps)(injectIntl(Bookmarks));
diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.tsx b/app/javascript/mastodon/features/bookmarked_statuses/index.tsx
new file mode 100644
index 0000000000..5d4574b05b
--- /dev/null
+++ b/app/javascript/mastodon/features/bookmarked_statuses/index.tsx
@@ -0,0 +1,116 @@
+import { useEffect, useRef, useCallback } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
+import {
+  fetchBookmarkedStatuses,
+  expandBookmarkedStatuses,
+} from 'mastodon/actions/bookmarks';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusList from 'mastodon/components/status_list';
+import { getStatusList } from 'mastodon/selectors';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const messages = defineMessages({
+  heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
+});
+
+const Bookmarks: React.FC<{
+  columnId: string;
+  multiColumn: boolean;
+}> = ({ columnId, multiColumn }) => {
+  const intl = useIntl();
+  const dispatch = useAppDispatch();
+  const columnRef = useRef<ColumnRef>(null);
+  const statusIds = useAppSelector((state) =>
+    getStatusList(state, 'bookmarks'),
+  );
+  const isLoading = useAppSelector(
+    (state) =>
+      state.status_lists.getIn(['bookmarks', 'isLoading'], true) as boolean,
+  );
+  const hasMore = useAppSelector(
+    (state) => !!state.status_lists.getIn(['bookmarks', 'next']),
+  );
+
+  useEffect(() => {
+    dispatch(fetchBookmarkedStatuses());
+  }, [dispatch]);
+
+  const handlePin = useCallback(() => {
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('BOOKMARKS', {}));
+    }
+  }, [dispatch, columnId]);
+
+  const handleMove = useCallback(
+    (dir: number) => {
+      dispatch(moveColumn(columnId, dir));
+    },
+    [dispatch, columnId],
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const handleLoadMore = useCallback(() => {
+    dispatch(expandBookmarkedStatuses());
+  }, [dispatch]);
+
+  const pinned = !!columnId;
+
+  const emptyMessage = (
+    <FormattedMessage
+      id='empty_column.bookmarked_statuses'
+      defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here."
+    />
+  );
+
+  return (
+    <Column
+      bindToDocument={!multiColumn}
+      ref={columnRef}
+      label={intl.formatMessage(messages.heading)}
+    >
+      <ColumnHeader
+        icon='bookmarks'
+        iconComponent={BookmarksIcon}
+        title={intl.formatMessage(messages.heading)}
+        onPin={handlePin}
+        onMove={handleMove}
+        onClick={handleHeaderClick}
+        pinned={pinned}
+        multiColumn={multiColumn}
+      />
+
+      <StatusList
+        trackScroll={!pinned}
+        statusIds={statusIds}
+        scrollKey={`bookmarked_statuses-${columnId}`}
+        hasMore={hasMore}
+        isLoading={isLoading}
+        onLoadMore={handleLoadMore}
+        emptyMessage={emptyMessage}
+        bindToDocument={!multiColumn}
+        timelineId='bookmarks'
+      />
+
+      <Helmet>
+        <title>{intl.formatMessage(messages.heading)}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default Bookmarks;
diff --git a/app/javascript/mastodon/features/circle_statuses/index.jsx b/app/javascript/mastodon/features/circle_statuses/index.jsx
deleted file mode 100644
index 1c0ee455e8..0000000000
--- a/app/javascript/mastodon/features/circle_statuses/index.jsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-
-import { Helmet } from 'react-helmet';
-import { withRouter } from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-
-import { debounce } from 'lodash';
-
-import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
-import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
-import EditIcon from '@/material-icons/400-24px/edit.svg?react';
-import { deleteCircle, expandCircleStatuses, fetchCircle, fetchCircleStatuses } from 'mastodon/actions/circles';
-import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
-import { openModal } from 'mastodon/actions/modal';
-import Column from 'mastodon/components/column';
-import ColumnHeader from 'mastodon/components/column_header';
-import { Icon }  from 'mastodon/components/icon';
-import { LoadingIndicator } from 'mastodon/components/loading_indicator';
-import StatusList from 'mastodon/components/status_list';
-import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
-import { getCircleStatusList } from 'mastodon/selectors';
-import { WithRouterPropTypes } from 'mastodon/utils/react_router';
-
-
-const messages = defineMessages({
-  deleteMessage: { id: 'confirmations.delete_circle.message', defaultMessage: 'Are you sure you want to permanently delete this circle?' },
-  deleteConfirm: { id: 'confirmations.delete_circle.confirm', defaultMessage: 'Delete' },
-  heading: { id: 'column.circles', defaultMessage: 'Circles' },
-});
-
-const mapStateToProps = (state, { params }) => ({
-  circle: state.getIn(['circles', params.id]),
-  statusIds: getCircleStatusList(state, params.id),
-  isLoading: state.getIn(['status_lists', 'circle_statuses', params.id, 'isLoading'], true),
-  hasMore: !!state.getIn(['status_lists', 'circle_statuses', params.id, 'next']),
-});
-
-class CircleStatuses extends ImmutablePureComponent {
-
-  static propTypes = {
-    params: PropTypes.object.isRequired,
-    dispatch: PropTypes.func.isRequired,
-    statusIds: ImmutablePropTypes.list.isRequired,
-    circle: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
-    intl: PropTypes.object.isRequired,
-    columnId: PropTypes.string,
-    multiColumn: PropTypes.bool,
-    hasMore: PropTypes.bool,
-    isLoading: PropTypes.bool,
-    ...WithRouterPropTypes,
-  };
-
-  UNSAFE_componentWillMount () {
-    this.props.dispatch(fetchCircle(this.props.params.id));
-    this.props.dispatch(fetchCircleStatuses(this.props.params.id));
-  }
-
-  UNSAFE_componentWillReceiveProps (nextProps) {
-    const { dispatch } = this.props;
-    const { id } = nextProps.params;
-
-    if (id !== this.props.params.id) {
-      dispatch(fetchCircle(id));
-      dispatch(fetchCircleStatuses(id));
-    }
-  }
-
-  handlePin = () => {
-    const { columnId, dispatch } = this.props;
-
-    if (columnId) {
-      dispatch(removeColumn(columnId));
-    } else {
-      dispatch(addColumn('CIRCLE_STATUSES', { id: this.props.params.id }));
-      this.props.history.push('/');
-    }
-  };
-
-  handleMove = (dir) => {
-    const { columnId, dispatch } = this.props;
-    dispatch(moveColumn(columnId, dir));
-  };
-
-  handleHeaderClick = () => {
-    this.column.scrollTop();
-  };
-
-  handleEditClick = () => {
-    this.props.history.push(`/circles/${this.props.params.id}/edit`);
-  };
-
-  handleDeleteClick = () => {
-    const { dispatch, columnId, intl } = this.props;
-    const { id } = this.props.params;
-
-    dispatch(openModal({
-      modalType: 'CONFIRM',
-      modalProps: {
-        message: intl.formatMessage(messages.deleteMessage),
-        confirm: intl.formatMessage(messages.deleteConfirm),
-        onConfirm: () => {
-          dispatch(deleteCircle(id));
-
-          if (columnId) {
-            dispatch(removeColumn(columnId));
-          } else {
-            this.props.history.push('/circles');
-          }
-        },
-      },
-    }));
-  };
-
-  setRef = c => {
-    this.column = c;
-  };
-
-  handleLoadMore = debounce(() => {
-    this.props.dispatch(expandCircleStatuses(this.props.params.id));
-  }, 300, { leading: true });
-
-  render () {
-    const { intl, circle, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
-    const pinned = !!columnId;
-
-    if (typeof circle === 'undefined') {
-      return (
-        <Column>
-          <div className='scrollable'>
-            <LoadingIndicator />
-          </div>
-        </Column>
-      );
-    } else if (circle === false) {
-      return (
-        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
-      );
-    }
-
-    const emptyMessage = <FormattedMessage id='empty_column.circle_statuses' defaultMessage="You don't have any circle posts yet. When you post one as circle, it will show up here." />;
-
-    return (
-      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
-        <ColumnHeader
-          icon='user-circle'
-          iconComponent={CircleIcon}
-          title={circle.get('title')}
-          onPin={this.handlePin}
-          onMove={this.handleMove}
-          onClick={this.handleHeaderClick}
-          pinned={pinned}
-          multiColumn={multiColumn}
-        >
-          <div className='column-settings'>
-            <section className='column-header__links'>
-              <button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
-                <Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='circles.edit' defaultMessage='Edit circle' />
-              </button>
-
-              <button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
-                <Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='circles.delete' defaultMessage='Delete circle' />
-              </button>
-            </section>
-          </div>
-        </ColumnHeader>
-
-        <StatusList
-          trackScroll={!pinned}
-          statusIds={statusIds}
-          scrollKey={`circle_statuses-${columnId}`}
-          hasMore={hasMore}
-          isLoading={isLoading}
-          onLoadMore={this.handleLoadMore}
-          emptyMessage={emptyMessage}
-          bindToDocument={!multiColumn}
-        />
-
-        <Helmet>
-          <title>{intl.formatMessage(messages.heading)}</title>
-          <meta name='robots' content='noindex' />
-        </Helmet>
-      </Column>
-    );
-  }
-
-}
-
-export default withRouter(connect(mapStateToProps)(injectIntl(CircleStatuses)));
diff --git a/app/javascript/mastodon/features/circle_statuses/index.tsx b/app/javascript/mastodon/features/circle_statuses/index.tsx
new file mode 100644
index 0000000000..2f962a5f13
--- /dev/null
+++ b/app/javascript/mastodon/features/circle_statuses/index.tsx
@@ -0,0 +1,118 @@
+import { useEffect, useRef, useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+import { useParams } from 'react-router';
+
+import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
+import {
+  expandCircleStatuses,
+  fetchCircle,
+  fetchCircleStatuses,
+} from 'mastodon/actions/circles';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusList from 'mastodon/components/status_list';
+import { getSubStatusList } from 'mastodon/selectors';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const CircleStatuses: React.FC<{
+  columnId: string;
+  multiColumn: boolean;
+}> = ({ columnId, multiColumn }) => {
+  const dispatch = useAppDispatch();
+  const { id } = useParams<{ id: string }>();
+  const columnRef = useRef<ColumnRef>(null);
+  const statusIds = useAppSelector((state) =>
+    getSubStatusList(state, 'circle', id),
+  );
+  const isLoading = useAppSelector(
+    (state) =>
+      state.status_lists.getIn(
+        ['circle_statuses', id, 'isLoading'],
+        true,
+      ) as boolean,
+  );
+  const hasMore = useAppSelector(
+    (state) => !!state.status_lists.getIn(['circle_statuses', id, 'next']),
+  );
+  const circle = useAppSelector((state) => state.circles.get(id));
+
+  useEffect(() => {
+    dispatch(fetchCircle(id));
+    dispatch(fetchCircleStatuses(id));
+  }, [dispatch, id]);
+
+  const handlePin = useCallback(() => {
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('CIRCLE_STATUSES', { id }));
+    }
+  }, [dispatch, columnId, id]);
+
+  const handleMove = useCallback(
+    (dir: number) => {
+      dispatch(moveColumn(columnId, dir));
+    },
+    [dispatch, columnId],
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const handleLoadMore = useCallback(() => {
+    dispatch(expandCircleStatuses(id));
+  }, [dispatch, id]);
+
+  const pinned = !!columnId;
+
+  const emptyMessage = (
+    <FormattedMessage
+      id='empty_column.circle_statuses'
+      defaultMessage="You don't have any circle posts yet. When you post one as circle, it will show up here."
+    />
+  );
+
+  return (
+    <Column
+      bindToDocument={!multiColumn}
+      ref={columnRef}
+      label={circle?.get('title')}
+    >
+      <ColumnHeader
+        icon='bookmark'
+        iconComponent={CircleIcon}
+        title={circle?.get('title')}
+        onPin={handlePin}
+        onMove={handleMove}
+        onClick={handleHeaderClick}
+        pinned={pinned}
+        multiColumn={multiColumn}
+      />
+
+      <StatusList
+        trackScroll={!pinned}
+        statusIds={statusIds}
+        scrollKey={`bookmark_ex_statuses-${columnId}`}
+        hasMore={hasMore}
+        isLoading={isLoading}
+        onLoadMore={handleLoadMore}
+        emptyMessage={emptyMessage}
+        bindToDocument={!multiColumn}
+      />
+
+      <Helmet>
+        <title>{circle?.get('title')}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default CircleStatuses;
diff --git a/app/javascript/mastodon/features/circles/index.tsx b/app/javascript/mastodon/features/circles/index.tsx
index e1d02e1633..38291febc7 100644
--- a/app/javascript/mastodon/features/circles/index.tsx
+++ b/app/javascript/mastodon/features/circles/index.tsx
@@ -13,9 +13,9 @@ import { fetchCircles } from 'mastodon/actions/circles';
 import { openModal } from 'mastodon/actions/modal';
 import { Column } from 'mastodon/components/column';
 import { ColumnHeader } from 'mastodon/components/column_header';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { Icon } from 'mastodon/components/icon';
 import ScrollableList from 'mastodon/components/scrollable_list';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
 import { getOrderedCircles } from 'mastodon/selectors/circles';
 import { useAppSelector, useAppDispatch } from 'mastodon/store';
 
@@ -60,12 +60,11 @@ const CircleItem: React.FC<{
         <span>{title}</span>
       </Link>
 
-      <DropdownMenuContainer
+      <Dropdown
         scrollKey='circles'
         items={menu}
-        icons='ellipsis-h'
+        icon='ellipsis-h'
         iconComponent={MoreHorizIcon}
-        direction='right'
         title={intl.formatMessage(messages.more)}
       />
     </div>
diff --git a/app/javascript/mastodon/features/compose/components/action_bar.jsx b/app/javascript/mastodon/features/compose/components/action_bar.jsx
deleted file mode 100644
index fae7736855..0000000000
--- a/app/javascript/mastodon/features/compose/components/action_bar.jsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { useCallback } from 'react';
-
-import { defineMessages, useIntl } from 'react-intl';
-
-import { useDispatch } from 'react-redux';
-
-import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
-import { openModal } from 'mastodon/actions/modal';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
-
-const messages = defineMessages({
-  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
-  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
-  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
-  reaction_deck: { id: 'navigation_bar.reaction_deck', defaultMessage: 'Reaction deck' },
-  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
-  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
-  emoji_reactions: { id: 'navigation_bar.emoji_reactions', defaultMessage: 'Stamps' },
-  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
-  followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
-  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
-  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
-  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
-  filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
-  logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
-  bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
-});
-
-export const ActionBar = () => {
-  const dispatch = useDispatch();
-  const intl = useIntl();
-
-  const handleLogoutClick = useCallback(() => {
-    dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
-  }, [dispatch]);
-
-  let menu = [];
-
-  menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
-  menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
-  menu.push({ text: intl.formatMessage(messages.reaction_deck), to: '/reaction_deck' });
-  menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
-  menu.push(null);
-  menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
-  menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
-  menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
-  menu.push({ text: intl.formatMessage(messages.emoji_reactions), to: '/emoji_reactions' });
-  menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
-  menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
-  menu.push(null);
-  menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
-  menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
-  menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
-  menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
-  menu.push(null);
-  menu.push({ text: intl.formatMessage(messages.logout), action: handleLogoutClick });
-
-  return (
-    <DropdownMenuContainer
-      items={menu}
-      icon='bars'
-      iconComponent={MoreHorizIcon}
-      size={24}
-      direction='right'
-    />
-  );
-};
diff --git a/app/javascript/mastodon/features/compose/components/action_bar.tsx b/app/javascript/mastodon/features/compose/components/action_bar.tsx
new file mode 100644
index 0000000000..4a92485209
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/components/action_bar.tsx
@@ -0,0 +1,95 @@
+import { useMemo } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
+import { openModal } from 'mastodon/actions/modal';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
+import { useAppDispatch } from 'mastodon/store';
+
+const messages = defineMessages({
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
+  preferences: {
+    id: 'navigation_bar.preferences',
+    defaultMessage: 'Preferences',
+  },
+  reaction_deck: {
+    id: 'navigation_bar.reaction_deck',
+    defaultMessage: 'Reaction deck',
+  },
+  follow_requests: {
+    id: 'navigation_bar.follow_requests',
+    defaultMessage: 'Follow requests',
+  },
+  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
+  emoji_reactions: {
+    id: 'navigation_bar.emoji_reactions',
+    defaultMessage: 'Stamps',
+  },
+  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
+  followed_tags: {
+    id: 'navigation_bar.followed_tags',
+    defaultMessage: 'Followed hashtags',
+  },
+  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
+  domain_blocks: {
+    id: 'navigation_bar.domain_blocks',
+    defaultMessage: 'Blocked domains',
+  },
+  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
+  filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
+  logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
+  bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
+});
+
+export const ActionBar: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const intl = useIntl();
+
+  const menu = useMemo(() => {
+    const handleLogoutClick = () => {
+      dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} }));
+    };
+
+    return [
+      {
+        text: intl.formatMessage(messages.edit_profile),
+        href: '/settings/profile',
+      },
+      {
+        text: intl.formatMessage(messages.preferences),
+        href: '/settings/preferences',
+      },
+      { text: intl.formatMessage(messages.pins), to: '/pinned' },
+      null,
+      {
+        text: intl.formatMessage(messages.follow_requests),
+        to: '/follow_requests',
+      },
+      { text: intl.formatMessage(messages.favourites), to: '/favourites' },
+      { text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' },
+      {
+        text: intl.formatMessage(messages.emoji_reactions),
+        to: '/emoji_reactions',
+      },
+      { text: intl.formatMessage(messages.lists), to: '/lists' },
+      {
+        text: intl.formatMessage(messages.followed_tags),
+        to: '/followed_tags',
+      },
+      null,
+      { text: intl.formatMessage(messages.mutes), to: '/mutes' },
+      { text: intl.formatMessage(messages.blocks), to: '/blocks' },
+      {
+        text: intl.formatMessage(messages.domain_blocks),
+        to: '/domain_blocks',
+      },
+      { text: intl.formatMessage(messages.filters), href: '/filters' },
+      null,
+      { text: intl.formatMessage(messages.logout), action: handleLogoutClick },
+    ];
+  }, [intl, dispatch]);
+
+  return <Dropdown items={menu} icon='bars' iconComponent={MoreHorizIcon} />;
+};
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx
index 2396d7a5f5..889e7951d8 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.jsx
+++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx
@@ -25,7 +25,6 @@ import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
 import SearchabilityDropdownContainer from '../containers/searchability_dropdown_container';
 import SpoilerButtonContainer from '../containers/spoiler_button_container';
 import UploadButtonContainer from '../containers/upload_button_container';
-import WarningContainer from '../containers/warning_container';
 import { countableText } from '../util/counter';
 
 import { CharacterCounter } from './character_counter';
@@ -35,6 +34,7 @@ import { NavigationBar } from './navigation_bar';
 import { PollForm } from "./poll_form";
 import { ReplyIndicator } from './reply_indicator';
 import { UploadForm } from './upload_form';
+import { Warning } from './warning';
 
 const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
 
@@ -129,7 +129,7 @@ class ComposeForm extends ImmutablePureComponent {
       return;
     }
 
-    this.props.onSubmit(missingAltTextModal && this.props.missingAltText);
+    this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct');
 
     if (e) {
       e.preventDefault();
@@ -254,7 +254,7 @@ class ComposeForm extends ImmutablePureComponent {
       <form className='compose-form' onSubmit={this.handleSubmit}>
         <ReplyIndicator />
         {!withoutNavigation && <NavigationBar />}
-        <WarningContainer />
+        <Warning />
 
         <div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
           <div className='compose-form__scrollable'>
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx
index dac873c8e4..175bb6ada9 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx
@@ -12,11 +12,14 @@ import Overlay from 'react-overlays/Overlay';
 
 import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
 import { IconButton } from 'mastodon/components/icon_button';
+import emojiCompressed from 'mastodon/features/emoji/emoji_compressed';
 import { assetHost } from 'mastodon/utils/config';
 
 import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
 import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
 
+const nimblePickerData = emojiCompressed[5];
+
 const messages = defineMessages({
   emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
   emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
@@ -37,15 +40,18 @@ let EmojiPicker, Emoji; // load asynchronously
 
 const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
 
-const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
+const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
 
 const notFoundFn = () => (
   <div className='emoji-mart-no-results'>
     <Emoji
+      data={nimblePickerData}
       emoji='sleuth_or_spy'
       set='twitter'
       size={32}
       sheetSize={32}
+      sheetColumns={62}
+      sheetRows={62}
       backgroundImageFn={backgroundImageFn}
     />
 
@@ -67,7 +73,7 @@ class ModifierPickerMenu extends PureComponent {
     this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
   };
 
-  UNSAFE_componentWillReceiveProps (nextProps) {
+  UNSAFE_componentWillReceiveProps(nextProps) {
     if (nextProps.active) {
       this.attachListeners();
     } else {
@@ -75,7 +81,7 @@ class ModifierPickerMenu extends PureComponent {
     }
   }
 
-  componentWillUnmount () {
+  componentWillUnmount() {
     this.removeListeners();
   }
 
@@ -85,12 +91,12 @@ class ModifierPickerMenu extends PureComponent {
     }
   };
 
-  attachListeners () {
+  attachListeners() {
     document.addEventListener('click', this.handleDocumentClick, { capture: true });
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
   }
 
-  removeListeners () {
+  removeListeners() {
     document.removeEventListener('click', this.handleDocumentClick, { capture: true });
     document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
   }
@@ -99,17 +105,17 @@ class ModifierPickerMenu extends PureComponent {
     this.node = c;
   };
 
-  render () {
+  render() {
     const { active } = this.props;
 
     return (
       <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
-        <button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
-        <button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
-        <button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
-        <button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
-        <button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
-        <button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
+        <button type='button' onClick={this.handleClick} data-index={1}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
+        <button type='button' onClick={this.handleClick} data-index={2}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
+        <button type='button' onClick={this.handleClick} data-index={3}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
+        <button type='button' onClick={this.handleClick} data-index={4}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
+        <button type='button' onClick={this.handleClick} data-index={5}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
+        <button type='button' onClick={this.handleClick} data-index={6}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
       </div>
     );
   }
@@ -139,12 +145,12 @@ class ModifierPicker extends PureComponent {
     this.props.onClose();
   };
 
-  render () {
+  render() {
     const { active, modifier } = this.props;
 
     return (
       <div className='emoji-picker-dropdown__modifiers'>
-        <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
+        <Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
         <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
       </div>
     );
@@ -184,7 +190,7 @@ class EmojiPickerMenuImpl extends PureComponent {
     }
   };
 
-  componentDidMount () {
+  componentDidMount() {
     document.addEventListener('click', this.handleDocumentClick, { capture: true });
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
 
@@ -199,7 +205,7 @@ class EmojiPickerMenuImpl extends PureComponent {
     });
   }
 
-  componentWillUnmount () {
+  componentWillUnmount() {
     document.removeEventListener('click', this.handleDocumentClick, { capture: true });
     document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
   }
@@ -252,7 +258,7 @@ class EmojiPickerMenuImpl extends PureComponent {
     this.props.onSkinTone(modifier);
   };
 
-  render () {
+  render() {
     const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
 
     if (loading) {
@@ -280,6 +286,9 @@ class EmojiPickerMenuImpl extends PureComponent {
     return (
       <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
         <EmojiPicker
+          data={nimblePickerData}
+          sheetColumns={62}
+          sheetRows={62}
           perLine={8}
           emojiSize={22}
           sheetSize={32}
@@ -346,7 +355,7 @@ class EmojiPickerDropdown extends PureComponent {
 
       EmojiPickerAsync().then(EmojiMart => {
         EmojiPicker = EmojiMart.Picker;
-        Emoji       = EmojiMart.Emoji;
+        Emoji = EmojiMart.Emoji;
 
         this.setState({ loading: false });
       }).catch(() => {
@@ -387,7 +396,7 @@ class EmojiPickerDropdown extends PureComponent {
     this.setState({ placement: state.placement });
   };
 
-  render () {
+  render() {
     const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, inverted } = this.props;
     const title = intl.formatMessage(messages.emoji);
     const { active, loading, placement } = this.state;
@@ -404,7 +413,7 @@ class EmojiPickerDropdown extends PureComponent {
         />
 
         <Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
-          {({ props, placement })=> (
+          {({ props, placement }) => (
             <div {...props} style={{ ...props.style }}>
               <div className={`dropdown-animation ${placement}`}>
                 <EmojiPickerMenu
diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx
index 21c4359981..d11891308f 100644
--- a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx
+++ b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx
@@ -378,6 +378,7 @@ export const LanguageDropdown: React.FC = () => {
     if (text.length > 20) {
       debouncedGuess(text, setGuess);
     } else {
+      debouncedGuess.cancel();
       setGuess('');
     }
   }, [text, setGuess]);
diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.jsx b/app/javascript/mastodon/features/compose/components/upload_progress.jsx
deleted file mode 100644
index fd0c8f4530..0000000000
--- a/app/javascript/mastodon/features/compose/components/upload_progress.jsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { FormattedMessage } from 'react-intl';
-
-import spring from 'react-motion/lib/spring';
-
-import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
-import { Icon }  from 'mastodon/components/icon';
-
-import Motion from '../../ui/util/optional_motion';
-
-export const UploadProgress = ({ active, progress, isProcessing }) => {
-  if (!active) {
-    return null;
-  }
-
-  let message;
-
-  if (isProcessing) {
-    message = <FormattedMessage id='upload_progress.processing' defaultMessage='Processing…' />;
-  } else {
-    message = <FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />;
-  }
-
-  return (
-    <div className='upload-progress'>
-      <Icon id='upload' icon={UploadFileIcon} />
-
-      <div className='upload-progress__message'>
-        {message}
-
-        <div className='upload-progress__backdrop'>
-          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
-            {({ width }) =>
-              <div className='upload-progress__tracker' style={{ width: `${width}%` }} />
-            }
-          </Motion>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-UploadProgress.propTypes = {
-  active: PropTypes.bool,
-  progress: PropTypes.number,
-  isProcessing: PropTypes.bool,
-};
diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.tsx b/app/javascript/mastodon/features/compose/components/upload_progress.tsx
new file mode 100644
index 0000000000..be15917784
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/components/upload_progress.tsx
@@ -0,0 +1,52 @@
+import { FormattedMessage } from 'react-intl';
+
+import { animated, useSpring } from '@react-spring/web';
+
+import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
+import { Icon } from 'mastodon/components/icon';
+import { reduceMotion } from 'mastodon/initial_state';
+
+interface UploadProgressProps {
+  active: boolean;
+  progress: number;
+  isProcessing?: boolean;
+}
+
+export const UploadProgress: React.FC<UploadProgressProps> = ({
+  active,
+  progress,
+  isProcessing = false,
+}) => {
+  const styles = useSpring({
+    from: { width: '0%' },
+    to: { width: `${progress}%` },
+    immediate: reduceMotion || !active, // If this is not active, update the UI immediately.
+  });
+  if (!active) {
+    return null;
+  }
+
+  return (
+    <div className='upload-progress'>
+      <Icon id='upload' icon={UploadFileIcon} />
+
+      <div className='upload-progress__message'>
+        {isProcessing ? (
+          <FormattedMessage
+            id='upload_progress.processing'
+            defaultMessage='Processing…'
+          />
+        ) : (
+          <FormattedMessage
+            id='upload_progress.label'
+            defaultMessage='Uploading…'
+          />
+        )}
+
+        <div className='upload-progress__backdrop'>
+          <animated.div className='upload-progress__tracker' style={styles} />
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/compose/components/warning.jsx b/app/javascript/mastodon/features/compose/components/warning.jsx
deleted file mode 100644
index c5babc30a5..0000000000
--- a/app/javascript/mastodon/features/compose/components/warning.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import spring from 'react-motion/lib/spring';
-
-import Motion from '../../ui/util/optional_motion';
-
-export default class Warning extends PureComponent {
-
-  static propTypes = {
-    message: PropTypes.node.isRequired,
-  };
-
-  render () {
-    const { message } = this.props;
-
-    return (
-      <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
-        {({ opacity, scaleX, scaleY }) => (
-          <div className='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
-            {message}
-          </div>
-        )}
-      </Motion>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/compose/components/warning.tsx b/app/javascript/mastodon/features/compose/components/warning.tsx
new file mode 100644
index 0000000000..36b971b044
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/components/warning.tsx
@@ -0,0 +1,147 @@
+import { FormattedMessage } from 'react-intl';
+
+import { createSelector } from '@reduxjs/toolkit';
+
+import { animated, useSpring } from '@react-spring/web';
+
+import { me } from 'mastodon/initial_state';
+import { useAppSelector } from 'mastodon/store';
+import type { RootState } from 'mastodon/store';
+import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
+import { MENTION_PATTERN_REGEX } from 'mastodon/utils/mentions';
+
+const selector = createSelector(
+  (state: RootState) => state.compose.get('privacy') as string,
+  (state: RootState) => !!state.accounts.getIn([me, 'locked']),
+  (state: RootState) => state.compose.get('text') as string,
+  (state: RootState) => state.compose.get('searchability') as string,
+  (state: RootState) => state.compose.get('limited_scope') as string,
+  (privacy, locked, text, searchability, limited_scope) => ({
+    needsLockWarning: privacy === 'private' && !locked,
+    hashtagWarning:
+      !['public', 'public_unlisted', 'login'].includes(privacy) &&
+      (privacy !== 'unlisted' || searchability !== 'public') &&
+      HASHTAG_PATTERN_REGEX.test(text),
+    directMessageWarning: privacy === 'direct',
+    searchabilityWarning: searchability === 'limited',
+    mentionWarning:
+      ['mutual', 'circle', 'limited'].includes(privacy) &&
+      MENTION_PATTERN_REGEX.test(text),
+    limitedPostWarning:
+      ['mutual', 'circle'].includes(privacy) && !limited_scope,
+  }),
+);
+
+export const Warning = () => {
+  const {
+    needsLockWarning,
+    hashtagWarning,
+    directMessageWarning,
+    searchabilityWarning,
+    mentionWarning,
+    limitedPostWarning,
+  } = useAppSelector(selector);
+  if (needsLockWarning) {
+    return (
+      <WarningMessage>
+        <FormattedMessage
+          id='compose_form.lock_disclaimer'
+          defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.'
+          values={{
+            locked: (
+              <a href='/settings/profile'>
+                <FormattedMessage
+                  id='compose_form.lock_disclaimer.lock'
+                  defaultMessage='locked'
+                />
+              </a>
+            ),
+          }}
+        />
+      </WarningMessage>
+    );
+  }
+
+  if (hashtagWarning) {
+    return (
+      <WarningMessage>
+        <FormattedMessage
+          id='compose_form.hashtag_warning'
+          defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag."
+        />
+      </WarningMessage>
+    );
+  }
+
+  if (directMessageWarning) {
+    return (
+      <WarningMessage>
+        <FormattedMessage
+          id='compose_form.encryption_warning'
+          defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.'
+        />{' '}
+        <a href='/terms' target='_blank'>
+          <FormattedMessage
+            id='compose_form.direct_message_warning_learn_more'
+            defaultMessage='Learn more'
+          />
+        </a>
+      </WarningMessage>
+    );
+  }
+
+  if (searchabilityWarning) {
+    return (
+      <WarningMessage>
+        <FormattedMessage
+          id='compose_form.searchability_warning'
+          defaultMessage='Self only searchability is not available other mastodon servers. Others can search your post.'
+        />
+      </WarningMessage>
+    );
+  }
+
+  if (mentionWarning) {
+    return (
+      <WarningMessage>
+        <FormattedMessage
+          id='compose_form.mention_warning'
+          defaultMessage='When you add a mention to a limited post, the person you are mentioning can also see this post.'
+        />
+      </WarningMessage>
+    );
+  }
+
+  if (limitedPostWarning) {
+    return (
+      <WarningMessage>
+        <FormattedMessage
+          id='compose_form.limited_post_warning'
+          defaultMessage='Limited posts are NOT reached Misskey, normal Mastodon or so on.'
+        />
+      </WarningMessage>
+    );
+  }
+
+  return null;
+};
+
+export const WarningMessage: React.FC<React.PropsWithChildren> = ({
+  children,
+}) => {
+  const styles = useSpring({
+    from: {
+      opacity: 0,
+      transform: 'scale(0.85, 0.75)',
+    },
+    to: {
+      opacity: 1,
+      transform: 'scale(1, 1)',
+    },
+  });
+  return (
+    <animated.div className='compose-form__warning' style={styles}>
+      {children}
+    </animated.div>
+  );
+};
diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.jsx b/app/javascript/mastodon/features/compose/containers/warning_container.jsx
deleted file mode 100644
index bc74d1209b..0000000000
--- a/app/javascript/mastodon/features/compose/containers/warning_container.jsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { FormattedMessage } from 'react-intl';
-
-import { connect } from 'react-redux';
-
-import { me } from 'mastodon/initial_state';
-import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
-import { MENTION_PATTERN_REGEX } from 'mastodon/utils/mentions';
-
-import Warning from '../components/warning';
-
-const mapStateToProps = state => ({
-  needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
-  hashtagWarning: !['public', 'public_unlisted', 'login'].includes(state.getIn(['compose', 'privacy'])) && state.getIn(['compose', 'searchability']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
-  directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
-  searchabilityWarning: state.getIn(['compose', 'searchability']) === 'limited',
-  mentionWarning: ['mutual', 'circle', 'limited'].includes(state.getIn(['compose', 'privacy'])) && MENTION_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
-  limitedPostWarning: ['mutual', 'circle'].includes(state.getIn(['compose', 'privacy'])) && !state.getIn(['compose', 'limited_scope']),
-});
-
-const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning, searchabilityWarning, mentionWarning, limitedPostWarning }) => {
-  if (needsLockWarning) {
-    return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
-  }
-
-  if (hashtagWarning) {
-    return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag." />} />;
-  }
-
-  if (directMessageWarning) {
-    const message = (
-      <span>
-        <FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>
-      </span>
-    );
-
-    return <Warning message={message} />;
-  }
-
-  if (searchabilityWarning) {
-    return <Warning message={<FormattedMessage id='compose_form.searchability_warning' defaultMessage='Self only searchability is not available other mastodon servers. Others can search your post.' />} />;
-  }
-
-  if (mentionWarning) {
-    return <Warning message={<FormattedMessage id='compose_form.mention_warning' defaultMessage='When you add a mention to a limited post, the person you are mentioning can also see this post.' />} />;
-  }
-
-  if (limitedPostWarning) {
-    return <Warning message={<FormattedMessage id='compose_form.limited_post_warning' defaultMessage='Limited posts are NOT reached Misskey, normal Mastodon or so on.' />} />;
-  }
-
-  return null;
-};
-
-WarningWrapper.propTypes = {
-  needsLockWarning: PropTypes.bool,
-  hashtagWarning: PropTypes.bool,
-  directMessageWarning: PropTypes.bool,
-  searchabilityWarning: PropTypes.bool,
-  mentionWarning: PropTypes.bool,
-  limitedPostWarning: PropTypes.bool,
-};
-
-export default connect(mapStateToProps)(WarningWrapper);
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
index 0d154db1e1..c27cd3727f 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
@@ -24,7 +24,7 @@ import AvatarComposite from 'mastodon/components/avatar_composite';
 import { IconButton } from 'mastodon/components/icon_button';
 import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
 import StatusContent from 'mastodon/components/status_content';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { autoPlayGif } from 'mastodon/initial_state';
 import { makeGetStatus } from 'mastodon/selectors';
 
@@ -205,7 +205,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
             <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={handleReply} />
 
             <div className='status__action-bar-dropdown'>
-              <DropdownMenuContainer
+              <Dropdown
                 scrollKey={scrollKey}
                 status={lastStatus}
                 items={menu}
diff --git a/app/javascript/mastodon/features/directory/index.tsx b/app/javascript/mastodon/features/directory/index.tsx
index ef2649a27f..1374f99235 100644
--- a/app/javascript/mastodon/features/directory/index.tsx
+++ b/app/javascript/mastodon/features/directory/index.tsx
@@ -22,10 +22,9 @@ import { LoadMore } from 'mastodon/components/load_more';
 import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 import { RadioButton } from 'mastodon/components/radio_button';
 import ScrollContainer from 'mastodon/containers/scroll_container';
+import { useSearchParam } from 'mastodon/hooks/useSearchParam';
 import { useAppDispatch, useAppSelector } from 'mastodon/store';
 
-import { useSearchParam } from '../../../hooks/useSearchParam';
-
 import { AccountCard } from './components/account_card';
 
 const messages = defineMessages({
diff --git a/app/javascript/mastodon/features/domain_blocks/index.jsx b/app/javascript/mastodon/features/domain_blocks/index.jsx
deleted file mode 100644
index 3656596806..0000000000
--- a/app/javascript/mastodon/features/domain_blocks/index.jsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import { Helmet } from 'react-helmet';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { debounce } from 'lodash';
-
-import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
-import { Domain } from 'mastodon/components/domain';
-
-import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
-import { LoadingIndicator } from '../../components/loading_indicator';
-import ScrollableList from '../../components/scrollable_list';
-import Column from '../ui/components/column';
-
-const messages = defineMessages({
-  heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
-});
-
-const mapStateToProps = state => ({
-  domains: state.getIn(['domain_lists', 'blocks', 'items']),
-  hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
-});
-
-class Blocks extends ImmutablePureComponent {
-
-  static propTypes = {
-    params: PropTypes.object.isRequired,
-    dispatch: PropTypes.func.isRequired,
-    hasMore: PropTypes.bool,
-    domains: ImmutablePropTypes.orderedSet,
-    intl: PropTypes.object.isRequired,
-    multiColumn: PropTypes.bool,
-  };
-
-  UNSAFE_componentWillMount () {
-    this.props.dispatch(fetchDomainBlocks());
-  }
-
-  handleLoadMore = debounce(() => {
-    this.props.dispatch(expandDomainBlocks());
-  }, 300, { leading: true });
-
-  render () {
-    const { intl, domains, hasMore, multiColumn } = this.props;
-
-    if (!domains) {
-      return (
-        <Column>
-          <LoadingIndicator />
-        </Column>
-      );
-    }
-
-    const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no blocked domains yet.' />;
-
-    return (
-      <Column bindToDocument={!multiColumn} icon='ban' iconComponent={BlockIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton>
-        <ScrollableList
-          scrollKey='domain_blocks'
-          onLoadMore={this.handleLoadMore}
-          hasMore={hasMore}
-          emptyMessage={emptyMessage}
-          bindToDocument={!multiColumn}
-        >
-          {domains.map(domain =>
-            <Domain key={domain} domain={domain} />,
-          )}
-        </ScrollableList>
-
-        <Helmet>
-          <meta name='robots' content='noindex' />
-        </Helmet>
-      </Column>
-    );
-  }
-
-}
-
-export default connect(mapStateToProps)(injectIntl(Blocks));
diff --git a/app/javascript/mastodon/features/domain_blocks/index.tsx b/app/javascript/mastodon/features/domain_blocks/index.tsx
new file mode 100644
index 0000000000..900aba4745
--- /dev/null
+++ b/app/javascript/mastodon/features/domain_blocks/index.tsx
@@ -0,0 +1,113 @@
+import { useEffect, useRef, useCallback, useState } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
+import { apiGetDomainBlocks } from 'mastodon/api/domain_blocks';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import { Domain } from 'mastodon/components/domain';
+import ScrollableList from 'mastodon/components/scrollable_list';
+
+const messages = defineMessages({
+  heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
+});
+
+const Blocks: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
+  const intl = useIntl();
+  const [domains, setDomains] = useState<string[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [next, setNext] = useState<string | undefined>();
+  const hasMore = !!next;
+  const columnRef = useRef<ColumnRef>(null);
+
+  useEffect(() => {
+    setLoading(true);
+
+    void apiGetDomainBlocks()
+      .then(({ domains, links }) => {
+        const next = links.refs.find((link) => link.rel === 'next');
+
+        setLoading(false);
+        setDomains(domains);
+        setNext(next?.uri);
+
+        return '';
+      })
+      .catch(() => {
+        setLoading(false);
+      });
+  }, [setLoading, setDomains, setNext]);
+
+  const handleLoadMore = useCallback(() => {
+    setLoading(true);
+
+    void apiGetDomainBlocks(next)
+      .then(({ domains, links }) => {
+        const next = links.refs.find((link) => link.rel === 'next');
+
+        setLoading(false);
+        setDomains((previousDomains) => [...previousDomains, ...domains]);
+        setNext(next?.uri);
+
+        return '';
+      })
+      .catch(() => {
+        setLoading(false);
+      });
+  }, [setLoading, setDomains, setNext, next]);
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const emptyMessage = (
+    <FormattedMessage
+      id='empty_column.domain_blocks'
+      defaultMessage='There are no blocked domains yet.'
+    />
+  );
+
+  return (
+    <Column
+      bindToDocument={!multiColumn}
+      ref={columnRef}
+      label={intl.formatMessage(messages.heading)}
+    >
+      <ColumnHeader
+        icon='ban'
+        iconComponent={BlockIcon}
+        title={intl.formatMessage(messages.heading)}
+        onClick={handleHeaderClick}
+        multiColumn={multiColumn}
+        showBackButton
+      />
+
+      <ScrollableList
+        scrollKey='domain_blocks'
+        onLoadMore={handleLoadMore}
+        hasMore={hasMore}
+        isLoading={loading}
+        showLoading={loading && domains.length === 0}
+        emptyMessage={emptyMessage}
+        trackScroll={!multiColumn}
+        bindToDocument={!multiColumn}
+      >
+        {domains.map((domain) => (
+          <Domain key={domain} domain={domain} />
+        ))}
+      </ScrollableList>
+
+      <Helmet>
+        <title>{intl.formatMessage(messages.heading)}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default Blocks;
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
index 022c9baaf7..35804de82a 100644
--- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -22,23 +22,23 @@ describe('emoji', () => {
 
     it('does unicode', () => {
       expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
-        '<picture><img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg"></picture>');
+        '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">');
       expect(emojify('👨‍👩‍👧‍👧')).toEqual(
-        '<picture><img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg"></picture>');
-      expect(emojify('👩‍👩‍👦')).toEqual('<picture><img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg"></picture>');
+        '<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">');
+      expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg">');
       expect(emojify('\u2757')).toEqual(
-        '<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
     });
 
     it('does multiple unicode', () => {
       expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
-        '<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
       expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
-        '<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture><picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
       expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
-        '<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
       expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
-        'foo <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> bar');
+        'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> bar');
     });
 
     it('ignores unicode inside of tags', () => {
@@ -46,16 +46,16 @@ describe('emoji', () => {
     });
 
     it('does multiple emoji properly (issue 5188)', () => {
-      expect(emojify('👌🌈💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture><picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
-      expect(emojify('👌 🌈 💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture> <picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture> <picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
+      expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
+      expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
     });
 
     it('does an emoji that has no shortcode', () => {
-      expect(emojify('👁‍🗨')).toEqual('<picture><img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg"></picture>');
+      expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg">');
     });
 
     it('does an emoji whose filename is irregular', () => {
-      expect(emojify('↙️')).toEqual('<picture><img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg"></picture>');
+      expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg">');
     });
 
     it('avoid emojifying on invisible text', () => {
@@ -67,11 +67,11 @@ describe('emoji', () => {
 
     it('avoid emojifying on invisible text with nested tags', () => {
       expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
-        .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
+        .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
       expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
-        .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
+        .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
       expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
-        .toEqual('<span class="invisible">😄<br>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
+        .toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
     });
 
     it('does not emojify emojis with textual presentation VS15 character', () => {
@@ -81,17 +81,17 @@ describe('emoji', () => {
 
     it('does a simple emoji properly', () => {
       expect(emojify('♀♂'))
-        .toEqual('<picture><img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"></picture><picture><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg"></picture>');
+        .toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg">');
     });
 
     it('does an emoji containing ZWJ properly', () => {
       expect(emojify('💂‍♀️💂‍♂️'))
-        .toEqual('<picture><img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"></picture><picture><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg"></picture>');
+        .toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
     });
 
     it('keeps ordering as expected (issue fixed by PR 20677)', () => {
       expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>'))
-        .toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>');
+        .toEqual('<p><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>');
     });
   });
 });
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index 1f469aced7..66dcd89488 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -97,30 +97,30 @@ const emojifyTextNode = (node, customEmojis) => {
       const { filename, shortCode } = unicodeMapping[unicode_emoji];
       const title = shortCode ? `:${shortCode}:` : '';
 
-      replacement = document.createElement('picture');
-
       const isSystemTheme = !!document.body?.classList.contains('theme-system');
 
-      if(isSystemTheme) {
-        let source = document.createElement('source');
-        source.setAttribute('media', '(prefers-color-scheme: dark)');
-        source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`);
-        replacement.appendChild(source);
-      }
+      const theme = (isSystemTheme || document.body?.classList.contains('theme-mastodon-light')) ? 'light' : 'dark';
 
-      let img = document.createElement('img');
+      const imageFilename = emojiFilename(filename, theme);
+
+      const img = document.createElement('img');
       img.setAttribute('draggable', 'false');
       img.setAttribute('class', 'emojione');
       img.setAttribute('alt', unicode_emoji);
       img.setAttribute('title', title);
+      img.setAttribute('src', `${assetHost}/emoji/${imageFilename}.svg`);
 
-      let theme = "light";
+      if (isSystemTheme && imageFilename !== emojiFilename(filename, 'dark')) {
+        replacement = document.createElement('picture');
 
-      if(!isSystemTheme && !document.body?.classList.contains('theme-mastodon-light'))
-        theme = "dark";
-
-      img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`);
-      replacement.appendChild(img);
+        const source = document.createElement('source');
+        source.setAttribute('media', '(prefers-color-scheme: dark)');
+        source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, 'dark')}.svg`);
+        replacement.appendChild(source);
+        replacement.appendChild(img);
+      } else {
+        replacement = img;
+      }
     }
 
     // Add the processed-up-to-now string and the emoji replacement
@@ -135,7 +135,7 @@ const emojifyTextNode = (node, customEmojis) => {
 };
 
 const emojifyNode = (node, customEmojis) => {
-  for (const child of node.childNodes) {
+  for (const child of Array.from(node.childNodes)) {
     switch(child.nodeType) {
     case Node.TEXT_NODE:
       emojifyTextNode(child, customEmojis);
diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.d.ts b/app/javascript/mastodon/features/emoji/emoji_compressed.d.ts
index bd41e5b838..4e8c28a4ee 100644
--- a/app/javascript/mastodon/features/emoji/emoji_compressed.d.ts
+++ b/app/javascript/mastodon/features/emoji/emoji_compressed.d.ts
@@ -45,6 +45,7 @@ type EmojiCompressed = [
   Category[],
   Data['aliases'],
   EmojisWithoutShortCodes,
+  Data,
 ];
 
 /*
diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.js b/app/javascript/mastodon/features/emoji/emoji_compressed.js
index 6c2a5963d6..10eb440a55 100644
--- a/app/javascript/mastodon/features/emoji/emoji_compressed.js
+++ b/app/javascript/mastodon/features/emoji/emoji_compressed.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/no-commonjs --
-   We need to use CommonJS here due to preval */
 // @preval
 // http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
 // This file contains the compressed version of the emoji data from
@@ -9,18 +7,91 @@
 
 // This version comment should be bumped each time the emoji data is changed
 // to ensure that the prevaled file is regenerated by Babel
-// version: 2
+// version: 3
 
-const { emojiIndex } = require('emoji-mart');
-let data = require('emoji-mart/data/all.json');
+// This json file contains the names of the categories.
+const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json');
+const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json');
 const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
+const _ = require('lodash');
+
 
 const emojiMap = require('./emoji_map.json');
+// This json file is downloaded from https://github.com/iamcal/emoji-data/
+// and is used to correct the sheet coordinates since we're using that repo's sheet
+const emojiSheetData = require('./emoji_sheet.json');
 const { unicodeToFilename } = require('./unicode_to_filename_s');
 const { unicodeToUnifiedName } = require('./unicode_to_unified_name_s');
 
-if(data.compressed) {
-  data = emojiMartUncompress(data);
+// Grabbed from `emoji_utils` to avoid circular dependency
+function unifiedToNative(unified) {
+  let unicodes = unified.split('-'),
+    codePoints = unicodes.map((u) => `0x${u}`);
+
+  return String.fromCodePoint(...codePoints);
+}
+
+let data = {
+  compressed: true,
+  categories: emojiMart5Data.categories.map(cat => {
+    return {
+      ...cat,
+      name: emojiMart5LocalesData.categories[cat.id]
+    };
+  }),
+  aliases: emojiMart5Data.aliases,
+  emojis: _(emojiMart5Data.emojis).values().map(emoji => {
+    let skin_variations = {};
+    const unified = emoji.skins[0].unified.toUpperCase();
+    const emojiFromRawData = emojiSheetData.find(e => e.unified === unified);
+
+    if (!emojiFromRawData) {
+      return undefined;
+    }
+
+    if (emoji.skins.length > 1) {
+      const [, ...nonDefaultSkins] = emoji.skins;
+      nonDefaultSkins.forEach(skin => {
+        const [matchingRawCodePoints,matchingRawEmoji] = Object.entries(emojiFromRawData.skin_variations).find((pair) => {
+          const [, value] = pair;
+          return value.unified.toLowerCase() === skin.unified;
+        });
+
+        if (matchingRawEmoji && matchingRawCodePoints) {
+          // At the time of writing, the json from `@emoji-mart/data` doesn't have data
+          // for emoji like `woman-heart-woman` with two different skin tones.
+          const skinToneCode = matchingRawCodePoints.split('-')[0];
+          skin_variations[skinToneCode] = {
+            unified: matchingRawEmoji.unified.toUpperCase(),
+            non_qualified: null,
+            sheet_x: matchingRawEmoji.sheet_x,
+            sheet_y: matchingRawEmoji.sheet_y,
+            has_img_twitter: true,
+            native: unifiedToNative(matchingRawEmoji.unified.toUpperCase())
+          };
+        }
+      });
+    }
+
+    return {
+      a: emoji.name,
+      b: unified,
+      c: undefined,
+      f: true,
+      j: [emoji.id, ...emoji.keywords],
+      k: [emojiFromRawData.sheet_x, emojiFromRawData.sheet_y],
+      m: emoji.emoticons?.[0],
+      l: emoji.emoticons,
+      o: emoji.version,
+      id: emoji.id,
+      skin_variations,
+      native: unifiedToNative(unified.toUpperCase())
+    };
+  }).compact().keyBy(e => e.id).mapValues(e => _.omit(e, 'id')).value()
+};
+
+if (data.compressed) {
+  emojiMartUncompress(data);
 }
 
 const emojiMartData = data;
@@ -32,15 +103,10 @@ const shortcodeMap   = {};
 const shortCodesToEmojiData = {};
 const emojisWithoutShortCodes = [];
 
-Object.keys(emojiIndex.emojis).forEach(key => {
-  let emoji = emojiIndex.emojis[key];
+Object.keys(emojiMart5Data.emojis).forEach(key => {
+  let emoji = emojiMart5Data.emojis[key];
 
-  // Emojis with skin tone modifiers are stored like this
-  if (Object.hasOwn(emoji, '1')) {
-    emoji = emoji['1'];
-  }
-
-  shortcodeMap[emoji.native] = emoji.id;
+  shortcodeMap[emoji.skins[0].native] = emoji.id;
 });
 
 const stripModifiers = unicode => {
@@ -84,13 +150,9 @@ Object.keys(emojiMap).forEach(key => {
   }
 });
 
-Object.keys(emojiIndex.emojis).forEach(key => {
-  let emoji = emojiIndex.emojis[key];
+Object.keys(emojiMartData.emojis).forEach(key => {
+  let emoji = emojiMartData.emojis[key];
 
-  // Emojis with skin tone modifiers are stored like this
-  if (Object.hasOwn(emoji, '1')) {
-    emoji = emoji['1'];
-  }
 
   const { native } = emoji;
   let { short_names, search, unified } = emojiMartData.emojis[key];
@@ -135,4 +197,5 @@ module.exports = JSON.parse(JSON.stringify([
   emojiMartData.categories,
   emojiMartData.aliases,
   emojisWithoutShortCodes,
+  emojiMartData
 ]));
diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts
index 806a3f8927..8eeb457055 100644
--- a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts
+++ b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts
@@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
 import emojiCompressed from './emoji_compressed';
 import { unicodeToUnifiedName } from './unicode_to_unified_name';
 
-type Emojis = {
-  [key in NonNullable<keyof ShortCodesToEmojiData>]: {
+type Emojis = Record<
+  NonNullable<keyof ShortCodesToEmojiData>,
+  {
     native: BaseEmoji['native'];
     search: Search;
     short_names: Emoji['short_names'];
     unified: Emoji['unified'];
-  };
-};
+  }
+>;
 
 const [
   shortCodesToEmojiData,
diff --git a/app/javascript/mastodon/features/emoji/emoji_picker.js b/app/javascript/mastodon/features/emoji/emoji_picker.js
index 8725d39ecd..76cbe447a5 100644
--- a/app/javascript/mastodon/features/emoji/emoji_picker.js
+++ b/app/javascript/mastodon/features/emoji/emoji_picker.js
@@ -1,5 +1,5 @@
-import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
-import Picker from 'emoji-mart/dist-es/components/picker/picker';
+import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
+import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
 
 export {
   Picker,
diff --git a/app/javascript/mastodon/features/emoji/emoji_sheet.json b/app/javascript/mastodon/features/emoji/emoji_sheet.json
new file mode 100644
index 0000000000..455b906cec
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji/emoji_sheet.json
@@ -0,0 +1 @@
+[{"unified":"0023-FE0F-20E3","sheet_x":0,"sheet_y":0,"skin_variations":{}},{"unified":"002A-FE0F-20E3","sheet_x":0,"sheet_y":1,"skin_variations":{}},{"unified":"0030-FE0F-20E3","sheet_x":0,"sheet_y":2,"skin_variations":{}},{"unified":"0031-FE0F-20E3","sheet_x":0,"sheet_y":3,"skin_variations":{}},{"unified":"0032-FE0F-20E3","sheet_x":0,"sheet_y":4,"skin_variations":{}},{"unified":"0033-FE0F-20E3","sheet_x":0,"sheet_y":5,"skin_variations":{}},{"unified":"0034-FE0F-20E3","sheet_x":0,"sheet_y":6,"skin_variations":{}},{"unified":"0035-FE0F-20E3","sheet_x":0,"sheet_y":7,"skin_variations":{}},{"unified":"0036-FE0F-20E3","sheet_x":0,"sheet_y":8,"skin_variations":{}},{"unified":"0037-FE0F-20E3","sheet_x":0,"sheet_y":9,"skin_variations":{}},{"unified":"0038-FE0F-20E3","sheet_x":0,"sheet_y":10,"skin_variations":{}},{"unified":"0039-FE0F-20E3","sheet_x":0,"sheet_y":11,"skin_variations":{}},{"unified":"00A9-FE0F","sheet_x":0,"sheet_y":12,"skin_variations":{}},{"unified":"00AE-FE0F","sheet_x":0,"sheet_y":13,"skin_variations":{}},{"unified":"1F004","sheet_x":0,"sheet_y":14,"skin_variations":{}},{"unified":"1F0CF","sheet_x":0,"sheet_y":15,"skin_variations":{}},{"unified":"1F170-FE0F","sheet_x":0,"sheet_y":16,"skin_variations":{}},{"unified":"1F171-FE0F","sheet_x":0,"sheet_y":17,"skin_variations":{}},{"unified":"1F17E-FE0F","sheet_x":0,"sheet_y":18,"skin_variations":{}},{"unified":"1F17F-FE0F","sheet_x":0,"sheet_y":19,"skin_variations":{}},{"unified":"1F18E","sheet_x":0,"sheet_y":20,"skin_variations":{}},{"unified":"1F191","sheet_x":0,"sheet_y":21,"skin_variations":{}},{"unified":"1F192","sheet_x":0,"sheet_y":22,"skin_variations":{}},{"unified":"1F193","sheet_x":0,"sheet_y":23,"skin_variations":{}},{"unified":"1F194","sheet_x":0,"sheet_y":24,"skin_variations":{}},{"unified":"1F195","sheet_x":0,"sheet_y":25,"skin_variations":{}},{"unified":"1F196","sheet_x":0,"sheet_y":26,"skin_variations":{}},{"unified":"1F197","sheet_x":0,"sheet_y":27,"skin_variations":{}},{"unified":"1F198","sheet_x":0,"sheet_y":28,"skin_variations":{}},{"unified":"1F199","sheet_x":0,"sheet_y":29,"skin_variations":{}},{"unified":"1F19A","sheet_x":0,"sheet_y":30,"skin_variations":{}},{"unified":"1F1E6-1F1E8","sheet_x":0,"sheet_y":31,"skin_variations":{}},{"unified":"1F1E6-1F1E9","sheet_x":0,"sheet_y":32,"skin_variations":{}},{"unified":"1F1E6-1F1EA","sheet_x":0,"sheet_y":33,"skin_variations":{}},{"unified":"1F1E6-1F1EB","sheet_x":0,"sheet_y":34,"skin_variations":{}},{"unified":"1F1E6-1F1EC","sheet_x":0,"sheet_y":35,"skin_variations":{}},{"unified":"1F1E6-1F1EE","sheet_x":0,"sheet_y":36,"skin_variations":{}},{"unified":"1F1E6-1F1F1","sheet_x":0,"sheet_y":37,"skin_variations":{}},{"unified":"1F1E6-1F1F2","sheet_x":0,"sheet_y":38,"skin_variations":{}},{"unified":"1F1E6-1F1F4","sheet_x":0,"sheet_y":39,"skin_variations":{}},{"unified":"1F1E6-1F1F6","sheet_x":0,"sheet_y":40,"skin_variations":{}},{"unified":"1F1E6-1F1F7","sheet_x":0,"sheet_y":41,"skin_variations":{}},{"unified":"1F1E6-1F1F8","sheet_x":0,"sheet_y":42,"skin_variations":{}},{"unified":"1F1E6-1F1F9","sheet_x":0,"sheet_y":43,"skin_variations":{}},{"unified":"1F1E6-1F1FA","sheet_x":0,"sheet_y":44,"skin_variations":{}},{"unified":"1F1E6-1F1FC","sheet_x":0,"sheet_y":45,"skin_variations":{}},{"unified":"1F1E6-1F1FD","sheet_x":0,"sheet_y":46,"skin_variations":{}},{"unified":"1F1E6-1F1FF","sheet_x":0,"sheet_y":47,"skin_variations":{}},{"unified":"1F1E7-1F1E6","sheet_x":0,"sheet_y":48,"skin_variations":{}},{"unified":"1F1E7-1F1E7","sheet_x":0,"sheet_y":49,"skin_variations":{}},{"unified":"1F1E7-1F1E9","sheet_x":0,"sheet_y":50,"skin_variations":{}},{"unified":"1F1E7-1F1EA","sheet_x":0,"sheet_y":51,"skin_variations":{}},{"unified":"1F1E7-1F1EB","sheet_x":0,"sheet_y":52,"skin_variations":{}},{"unified":"1F1E7-1F1EC","sheet_x":0,"sheet_y":53,"skin_variations":{}},{"unified":"1F1E7-1F1ED","sheet_x":0,"sheet_y":54,"skin_variations":{}},{"unified":"1F1E7-1F1EE","sheet_x":0,"sheet_y":55,"skin_variations":{}},{"unified":"1F1E7-1F1EF","sheet_x":0,"sheet_y":56,"skin_variations":{}},{"unified":"1F1E7-1F1F1","sheet_x":0,"sheet_y":57,"skin_variations":{}},{"unified":"1F1E7-1F1F2","sheet_x":0,"sheet_y":58,"skin_variations":{}},{"unified":"1F1E7-1F1F3","sheet_x":0,"sheet_y":59,"skin_variations":{}},{"unified":"1F1E7-1F1F4","sheet_x":0,"sheet_y":60,"skin_variations":{}},{"unified":"1F1E7-1F1F6","sheet_x":0,"sheet_y":61,"skin_variations":{}},{"unified":"1F1E7-1F1F7","sheet_x":1,"sheet_y":0,"skin_variations":{}},{"unified":"1F1E7-1F1F8","sheet_x":1,"sheet_y":1,"skin_variations":{}},{"unified":"1F1E7-1F1F9","sheet_x":1,"sheet_y":2,"skin_variations":{}},{"unified":"1F1E7-1F1FB","sheet_x":1,"sheet_y":3,"skin_variations":{}},{"unified":"1F1E7-1F1FC","sheet_x":1,"sheet_y":4,"skin_variations":{}},{"unified":"1F1E7-1F1FE","sheet_x":1,"sheet_y":5,"skin_variations":{}},{"unified":"1F1E7-1F1FF","sheet_x":1,"sheet_y":6,"skin_variations":{}},{"unified":"1F1E8-1F1E6","sheet_x":1,"sheet_y":7,"skin_variations":{}},{"unified":"1F1E8-1F1E8","sheet_x":1,"sheet_y":8,"skin_variations":{}},{"unified":"1F1E8-1F1E9","sheet_x":1,"sheet_y":9,"skin_variations":{}},{"unified":"1F1E8-1F1EB","sheet_x":1,"sheet_y":10,"skin_variations":{}},{"unified":"1F1E8-1F1EC","sheet_x":1,"sheet_y":11,"skin_variations":{}},{"unified":"1F1E8-1F1ED","sheet_x":1,"sheet_y":12,"skin_variations":{}},{"unified":"1F1E8-1F1EE","sheet_x":1,"sheet_y":13,"skin_variations":{}},{"unified":"1F1E8-1F1F0","sheet_x":1,"sheet_y":14,"skin_variations":{}},{"unified":"1F1E8-1F1F1","sheet_x":1,"sheet_y":15,"skin_variations":{}},{"unified":"1F1E8-1F1F2","sheet_x":1,"sheet_y":16,"skin_variations":{}},{"unified":"1F1E8-1F1F3","sheet_x":1,"sheet_y":17,"skin_variations":{}},{"unified":"1F1E8-1F1F4","sheet_x":1,"sheet_y":18,"skin_variations":{}},{"unified":"1F1E8-1F1F5","sheet_x":1,"sheet_y":19,"skin_variations":{}},{"unified":"1F1E8-1F1F7","sheet_x":1,"sheet_y":20,"skin_variations":{}},{"unified":"1F1E8-1F1FA","sheet_x":1,"sheet_y":21,"skin_variations":{}},{"unified":"1F1E8-1F1FB","sheet_x":1,"sheet_y":22,"skin_variations":{}},{"unified":"1F1E8-1F1FC","sheet_x":1,"sheet_y":23,"skin_variations":{}},{"unified":"1F1E8-1F1FD","sheet_x":1,"sheet_y":24,"skin_variations":{}},{"unified":"1F1E8-1F1FE","sheet_x":1,"sheet_y":25,"skin_variations":{}},{"unified":"1F1E8-1F1FF","sheet_x":1,"sheet_y":26,"skin_variations":{}},{"unified":"1F1E9-1F1EA","sheet_x":1,"sheet_y":27,"skin_variations":{}},{"unified":"1F1E9-1F1EC","sheet_x":1,"sheet_y":28,"skin_variations":{}},{"unified":"1F1E9-1F1EF","sheet_x":1,"sheet_y":29,"skin_variations":{}},{"unified":"1F1E9-1F1F0","sheet_x":1,"sheet_y":30,"skin_variations":{}},{"unified":"1F1E9-1F1F2","sheet_x":1,"sheet_y":31,"skin_variations":{}},{"unified":"1F1E9-1F1F4","sheet_x":1,"sheet_y":32,"skin_variations":{}},{"unified":"1F1E9-1F1FF","sheet_x":1,"sheet_y":33,"skin_variations":{}},{"unified":"1F1EA-1F1E6","sheet_x":1,"sheet_y":34,"skin_variations":{}},{"unified":"1F1EA-1F1E8","sheet_x":1,"sheet_y":35,"skin_variations":{}},{"unified":"1F1EA-1F1EA","sheet_x":1,"sheet_y":36,"skin_variations":{}},{"unified":"1F1EA-1F1EC","sheet_x":1,"sheet_y":37,"skin_variations":{}},{"unified":"1F1EA-1F1ED","sheet_x":1,"sheet_y":38,"skin_variations":{}},{"unified":"1F1EA-1F1F7","sheet_x":1,"sheet_y":39,"skin_variations":{}},{"unified":"1F1EA-1F1F8","sheet_x":1,"sheet_y":40,"skin_variations":{}},{"unified":"1F1EA-1F1F9","sheet_x":1,"sheet_y":41,"skin_variations":{}},{"unified":"1F1EA-1F1FA","sheet_x":1,"sheet_y":42,"skin_variations":{}},{"unified":"1F1EB-1F1EE","sheet_x":1,"sheet_y":43,"skin_variations":{}},{"unified":"1F1EB-1F1EF","sheet_x":1,"sheet_y":44,"skin_variations":{}},{"unified":"1F1EB-1F1F0","sheet_x":1,"sheet_y":45,"skin_variations":{}},{"unified":"1F1EB-1F1F2","sheet_x":1,"sheet_y":46,"skin_variations":{}},{"unified":"1F1EB-1F1F4","sheet_x":1,"sheet_y":47,"skin_variations":{}},{"unified":"1F1EB-1F1F7","sheet_x":1,"sheet_y":48,"skin_variations":{}},{"unified":"1F1EC-1F1E6","sheet_x":1,"sheet_y":49,"skin_variations":{}},{"unified":"1F1EC-1F1E7","sheet_x":1,"sheet_y":50,"skin_variations":{}},{"unified":"1F1EC-1F1E9","sheet_x":1,"sheet_y":51,"skin_variations":{}},{"unified":"1F1EC-1F1EA","sheet_x":1,"sheet_y":52,"skin_variations":{}},{"unified":"1F1EC-1F1EB","sheet_x":1,"sheet_y":53,"skin_variations":{}},{"unified":"1F1EC-1F1EC","sheet_x":1,"sheet_y":54,"skin_variations":{}},{"unified":"1F1EC-1F1ED","sheet_x":1,"sheet_y":55,"skin_variations":{}},{"unified":"1F1EC-1F1EE","sheet_x":1,"sheet_y":56,"skin_variations":{}},{"unified":"1F1EC-1F1F1","sheet_x":1,"sheet_y":57,"skin_variations":{}},{"unified":"1F1EC-1F1F2","sheet_x":1,"sheet_y":58,"skin_variations":{}},{"unified":"1F1EC-1F1F3","sheet_x":1,"sheet_y":59,"skin_variations":{}},{"unified":"1F1EC-1F1F5","sheet_x":1,"sheet_y":60,"skin_variations":{}},{"unified":"1F1EC-1F1F6","sheet_x":1,"sheet_y":61,"skin_variations":{}},{"unified":"1F1EC-1F1F7","sheet_x":2,"sheet_y":0,"skin_variations":{}},{"unified":"1F1EC-1F1F8","sheet_x":2,"sheet_y":1,"skin_variations":{}},{"unified":"1F1EC-1F1F9","sheet_x":2,"sheet_y":2,"skin_variations":{}},{"unified":"1F1EC-1F1FA","sheet_x":2,"sheet_y":3,"skin_variations":{}},{"unified":"1F1EC-1F1FC","sheet_x":2,"sheet_y":4,"skin_variations":{}},{"unified":"1F1EC-1F1FE","sheet_x":2,"sheet_y":5,"skin_variations":{}},{"unified":"1F1ED-1F1F0","sheet_x":2,"sheet_y":6,"skin_variations":{}},{"unified":"1F1ED-1F1F2","sheet_x":2,"sheet_y":7,"skin_variations":{}},{"unified":"1F1ED-1F1F3","sheet_x":2,"sheet_y":8,"skin_variations":{}},{"unified":"1F1ED-1F1F7","sheet_x":2,"sheet_y":9,"skin_variations":{}},{"unified":"1F1ED-1F1F9","sheet_x":2,"sheet_y":10,"skin_variations":{}},{"unified":"1F1ED-1F1FA","sheet_x":2,"sheet_y":11,"skin_variations":{}},{"unified":"1F1EE-1F1E8","sheet_x":2,"sheet_y":12,"skin_variations":{}},{"unified":"1F1EE-1F1E9","sheet_x":2,"sheet_y":13,"skin_variations":{}},{"unified":"1F1EE-1F1EA","sheet_x":2,"sheet_y":14,"skin_variations":{}},{"unified":"1F1EE-1F1F1","sheet_x":2,"sheet_y":15,"skin_variations":{}},{"unified":"1F1EE-1F1F2","sheet_x":2,"sheet_y":16,"skin_variations":{}},{"unified":"1F1EE-1F1F3","sheet_x":2,"sheet_y":17,"skin_variations":{}},{"unified":"1F1EE-1F1F4","sheet_x":2,"sheet_y":18,"skin_variations":{}},{"unified":"1F1EE-1F1F6","sheet_x":2,"sheet_y":19,"skin_variations":{}},{"unified":"1F1EE-1F1F7","sheet_x":2,"sheet_y":20,"skin_variations":{}},{"unified":"1F1EE-1F1F8","sheet_x":2,"sheet_y":21,"skin_variations":{}},{"unified":"1F1EE-1F1F9","sheet_x":2,"sheet_y":22,"skin_variations":{}},{"unified":"1F1EF-1F1EA","sheet_x":2,"sheet_y":23,"skin_variations":{}},{"unified":"1F1EF-1F1F2","sheet_x":2,"sheet_y":24,"skin_variations":{}},{"unified":"1F1EF-1F1F4","sheet_x":2,"sheet_y":25,"skin_variations":{}},{"unified":"1F1EF-1F1F5","sheet_x":2,"sheet_y":26,"skin_variations":{}},{"unified":"1F1F0-1F1EA","sheet_x":2,"sheet_y":27,"skin_variations":{}},{"unified":"1F1F0-1F1EC","sheet_x":2,"sheet_y":28,"skin_variations":{}},{"unified":"1F1F0-1F1ED","sheet_x":2,"sheet_y":29,"skin_variations":{}},{"unified":"1F1F0-1F1EE","sheet_x":2,"sheet_y":30,"skin_variations":{}},{"unified":"1F1F0-1F1F2","sheet_x":2,"sheet_y":31,"skin_variations":{}},{"unified":"1F1F0-1F1F3","sheet_x":2,"sheet_y":32,"skin_variations":{}},{"unified":"1F1F0-1F1F5","sheet_x":2,"sheet_y":33,"skin_variations":{}},{"unified":"1F1F0-1F1F7","sheet_x":2,"sheet_y":34,"skin_variations":{}},{"unified":"1F1F0-1F1FC","sheet_x":2,"sheet_y":35,"skin_variations":{}},{"unified":"1F1F0-1F1FE","sheet_x":2,"sheet_y":36,"skin_variations":{}},{"unified":"1F1F0-1F1FF","sheet_x":2,"sheet_y":37,"skin_variations":{}},{"unified":"1F1F1-1F1E6","sheet_x":2,"sheet_y":38,"skin_variations":{}},{"unified":"1F1F1-1F1E7","sheet_x":2,"sheet_y":39,"skin_variations":{}},{"unified":"1F1F1-1F1E8","sheet_x":2,"sheet_y":40,"skin_variations":{}},{"unified":"1F1F1-1F1EE","sheet_x":2,"sheet_y":41,"skin_variations":{}},{"unified":"1F1F1-1F1F0","sheet_x":2,"sheet_y":42,"skin_variations":{}},{"unified":"1F1F1-1F1F7","sheet_x":2,"sheet_y":43,"skin_variations":{}},{"unified":"1F1F1-1F1F8","sheet_x":2,"sheet_y":44,"skin_variations":{}},{"unified":"1F1F1-1F1F9","sheet_x":2,"sheet_y":45,"skin_variations":{}},{"unified":"1F1F1-1F1FA","sheet_x":2,"sheet_y":46,"skin_variations":{}},{"unified":"1F1F1-1F1FB","sheet_x":2,"sheet_y":47,"skin_variations":{}},{"unified":"1F1F1-1F1FE","sheet_x":2,"sheet_y":48,"skin_variations":{}},{"unified":"1F1F2-1F1E6","sheet_x":2,"sheet_y":49,"skin_variations":{}},{"unified":"1F1F2-1F1E8","sheet_x":2,"sheet_y":50,"skin_variations":{}},{"unified":"1F1F2-1F1E9","sheet_x":2,"sheet_y":51,"skin_variations":{}},{"unified":"1F1F2-1F1EA","sheet_x":2,"sheet_y":52,"skin_variations":{}},{"unified":"1F1F2-1F1EB","sheet_x":2,"sheet_y":53,"skin_variations":{}},{"unified":"1F1F2-1F1EC","sheet_x":2,"sheet_y":54,"skin_variations":{}},{"unified":"1F1F2-1F1ED","sheet_x":2,"sheet_y":55,"skin_variations":{}},{"unified":"1F1F2-1F1F0","sheet_x":2,"sheet_y":56,"skin_variations":{}},{"unified":"1F1F2-1F1F1","sheet_x":2,"sheet_y":57,"skin_variations":{}},{"unified":"1F1F2-1F1F2","sheet_x":2,"sheet_y":58,"skin_variations":{}},{"unified":"1F1F2-1F1F3","sheet_x":2,"sheet_y":59,"skin_variations":{}},{"unified":"1F1F2-1F1F4","sheet_x":2,"sheet_y":60,"skin_variations":{}},{"unified":"1F1F2-1F1F5","sheet_x":2,"sheet_y":61,"skin_variations":{}},{"unified":"1F1F2-1F1F6","sheet_x":3,"sheet_y":0,"skin_variations":{}},{"unified":"1F1F2-1F1F7","sheet_x":3,"sheet_y":1,"skin_variations":{}},{"unified":"1F1F2-1F1F8","sheet_x":3,"sheet_y":2,"skin_variations":{}},{"unified":"1F1F2-1F1F9","sheet_x":3,"sheet_y":3,"skin_variations":{}},{"unified":"1F1F2-1F1FA","sheet_x":3,"sheet_y":4,"skin_variations":{}},{"unified":"1F1F2-1F1FB","sheet_x":3,"sheet_y":5,"skin_variations":{}},{"unified":"1F1F2-1F1FC","sheet_x":3,"sheet_y":6,"skin_variations":{}},{"unified":"1F1F2-1F1FD","sheet_x":3,"sheet_y":7,"skin_variations":{}},{"unified":"1F1F2-1F1FE","sheet_x":3,"sheet_y":8,"skin_variations":{}},{"unified":"1F1F2-1F1FF","sheet_x":3,"sheet_y":9,"skin_variations":{}},{"unified":"1F1F3-1F1E6","sheet_x":3,"sheet_y":10,"skin_variations":{}},{"unified":"1F1F3-1F1E8","sheet_x":3,"sheet_y":11,"skin_variations":{}},{"unified":"1F1F3-1F1EA","sheet_x":3,"sheet_y":12,"skin_variations":{}},{"unified":"1F1F3-1F1EB","sheet_x":3,"sheet_y":13,"skin_variations":{}},{"unified":"1F1F3-1F1EC","sheet_x":3,"sheet_y":14,"skin_variations":{}},{"unified":"1F1F3-1F1EE","sheet_x":3,"sheet_y":15,"skin_variations":{}},{"unified":"1F1F3-1F1F1","sheet_x":3,"sheet_y":16,"skin_variations":{}},{"unified":"1F1F3-1F1F4","sheet_x":3,"sheet_y":17,"skin_variations":{}},{"unified":"1F1F3-1F1F5","sheet_x":3,"sheet_y":18,"skin_variations":{}},{"unified":"1F1F3-1F1F7","sheet_x":3,"sheet_y":19,"skin_variations":{}},{"unified":"1F1F3-1F1FA","sheet_x":3,"sheet_y":20,"skin_variations":{}},{"unified":"1F1F3-1F1FF","sheet_x":3,"sheet_y":21,"skin_variations":{}},{"unified":"1F1F4-1F1F2","sheet_x":3,"sheet_y":22,"skin_variations":{}},{"unified":"1F1F5-1F1E6","sheet_x":3,"sheet_y":23,"skin_variations":{}},{"unified":"1F1F5-1F1EA","sheet_x":3,"sheet_y":24,"skin_variations":{}},{"unified":"1F1F5-1F1EB","sheet_x":3,"sheet_y":25,"skin_variations":{}},{"unified":"1F1F5-1F1EC","sheet_x":3,"sheet_y":26,"skin_variations":{}},{"unified":"1F1F5-1F1ED","sheet_x":3,"sheet_y":27,"skin_variations":{}},{"unified":"1F1F5-1F1F0","sheet_x":3,"sheet_y":28,"skin_variations":{}},{"unified":"1F1F5-1F1F1","sheet_x":3,"sheet_y":29,"skin_variations":{}},{"unified":"1F1F5-1F1F2","sheet_x":3,"sheet_y":30,"skin_variations":{}},{"unified":"1F1F5-1F1F3","sheet_x":3,"sheet_y":31,"skin_variations":{}},{"unified":"1F1F5-1F1F7","sheet_x":3,"sheet_y":32,"skin_variations":{}},{"unified":"1F1F5-1F1F8","sheet_x":3,"sheet_y":33,"skin_variations":{}},{"unified":"1F1F5-1F1F9","sheet_x":3,"sheet_y":34,"skin_variations":{}},{"unified":"1F1F5-1F1FC","sheet_x":3,"sheet_y":35,"skin_variations":{}},{"unified":"1F1F5-1F1FE","sheet_x":3,"sheet_y":36,"skin_variations":{}},{"unified":"1F1F6-1F1E6","sheet_x":3,"sheet_y":37,"skin_variations":{}},{"unified":"1F1F7-1F1EA","sheet_x":3,"sheet_y":38,"skin_variations":{}},{"unified":"1F1F7-1F1F4","sheet_x":3,"sheet_y":39,"skin_variations":{}},{"unified":"1F1F7-1F1F8","sheet_x":3,"sheet_y":40,"skin_variations":{}},{"unified":"1F1F7-1F1FA","sheet_x":3,"sheet_y":41,"skin_variations":{}},{"unified":"1F1F7-1F1FC","sheet_x":3,"sheet_y":42,"skin_variations":{}},{"unified":"1F1F8-1F1E6","sheet_x":3,"sheet_y":43,"skin_variations":{}},{"unified":"1F1F8-1F1E7","sheet_x":3,"sheet_y":44,"skin_variations":{}},{"unified":"1F1F8-1F1E8","sheet_x":3,"sheet_y":45,"skin_variations":{}},{"unified":"1F1F8-1F1E9","sheet_x":3,"sheet_y":46,"skin_variations":{}},{"unified":"1F1F8-1F1EA","sheet_x":3,"sheet_y":47,"skin_variations":{}},{"unified":"1F1F8-1F1EC","sheet_x":3,"sheet_y":48,"skin_variations":{}},{"unified":"1F1F8-1F1ED","sheet_x":3,"sheet_y":49,"skin_variations":{}},{"unified":"1F1F8-1F1EE","sheet_x":3,"sheet_y":50,"skin_variations":{}},{"unified":"1F1F8-1F1EF","sheet_x":3,"sheet_y":51,"skin_variations":{}},{"unified":"1F1F8-1F1F0","sheet_x":3,"sheet_y":52,"skin_variations":{}},{"unified":"1F1F8-1F1F1","sheet_x":3,"sheet_y":53,"skin_variations":{}},{"unified":"1F1F8-1F1F2","sheet_x":3,"sheet_y":54,"skin_variations":{}},{"unified":"1F1F8-1F1F3","sheet_x":3,"sheet_y":55,"skin_variations":{}},{"unified":"1F1F8-1F1F4","sheet_x":3,"sheet_y":56,"skin_variations":{}},{"unified":"1F1F8-1F1F7","sheet_x":3,"sheet_y":57,"skin_variations":{}},{"unified":"1F1F8-1F1F8","sheet_x":3,"sheet_y":58,"skin_variations":{}},{"unified":"1F1F8-1F1F9","sheet_x":3,"sheet_y":59,"skin_variations":{}},{"unified":"1F1F8-1F1FB","sheet_x":3,"sheet_y":60,"skin_variations":{}},{"unified":"1F1F8-1F1FD","sheet_x":3,"sheet_y":61,"skin_variations":{}},{"unified":"1F1F8-1F1FE","sheet_x":4,"sheet_y":0,"skin_variations":{}},{"unified":"1F1F8-1F1FF","sheet_x":4,"sheet_y":1,"skin_variations":{}},{"unified":"1F1F9-1F1E6","sheet_x":4,"sheet_y":2,"skin_variations":{}},{"unified":"1F1F9-1F1E8","sheet_x":4,"sheet_y":3,"skin_variations":{}},{"unified":"1F1F9-1F1E9","sheet_x":4,"sheet_y":4,"skin_variations":{}},{"unified":"1F1F9-1F1EB","sheet_x":4,"sheet_y":5,"skin_variations":{}},{"unified":"1F1F9-1F1EC","sheet_x":4,"sheet_y":6,"skin_variations":{}},{"unified":"1F1F9-1F1ED","sheet_x":4,"sheet_y":7,"skin_variations":{}},{"unified":"1F1F9-1F1EF","sheet_x":4,"sheet_y":8,"skin_variations":{}},{"unified":"1F1F9-1F1F0","sheet_x":4,"sheet_y":9,"skin_variations":{}},{"unified":"1F1F9-1F1F1","sheet_x":4,"sheet_y":10,"skin_variations":{}},{"unified":"1F1F9-1F1F2","sheet_x":4,"sheet_y":11,"skin_variations":{}},{"unified":"1F1F9-1F1F3","sheet_x":4,"sheet_y":12,"skin_variations":{}},{"unified":"1F1F9-1F1F4","sheet_x":4,"sheet_y":13,"skin_variations":{}},{"unified":"1F1F9-1F1F7","sheet_x":4,"sheet_y":14,"skin_variations":{}},{"unified":"1F1F9-1F1F9","sheet_x":4,"sheet_y":15,"skin_variations":{}},{"unified":"1F1F9-1F1FB","sheet_x":4,"sheet_y":16,"skin_variations":{}},{"unified":"1F1F9-1F1FC","sheet_x":4,"sheet_y":17,"skin_variations":{}},{"unified":"1F1F9-1F1FF","sheet_x":4,"sheet_y":18,"skin_variations":{}},{"unified":"1F1FA-1F1E6","sheet_x":4,"sheet_y":19,"skin_variations":{}},{"unified":"1F1FA-1F1EC","sheet_x":4,"sheet_y":20,"skin_variations":{}},{"unified":"1F1FA-1F1F2","sheet_x":4,"sheet_y":21,"skin_variations":{}},{"unified":"1F1FA-1F1F3","sheet_x":4,"sheet_y":22,"skin_variations":{}},{"unified":"1F1FA-1F1F8","sheet_x":4,"sheet_y":23,"skin_variations":{}},{"unified":"1F1FA-1F1FE","sheet_x":4,"sheet_y":24,"skin_variations":{}},{"unified":"1F1FA-1F1FF","sheet_x":4,"sheet_y":25,"skin_variations":{}},{"unified":"1F1FB-1F1E6","sheet_x":4,"sheet_y":26,"skin_variations":{}},{"unified":"1F1FB-1F1E8","sheet_x":4,"sheet_y":27,"skin_variations":{}},{"unified":"1F1FB-1F1EA","sheet_x":4,"sheet_y":28,"skin_variations":{}},{"unified":"1F1FB-1F1EC","sheet_x":4,"sheet_y":29,"skin_variations":{}},{"unified":"1F1FB-1F1EE","sheet_x":4,"sheet_y":30,"skin_variations":{}},{"unified":"1F1FB-1F1F3","sheet_x":4,"sheet_y":31,"skin_variations":{}},{"unified":"1F1FB-1F1FA","sheet_x":4,"sheet_y":32,"skin_variations":{}},{"unified":"1F1FC-1F1EB","sheet_x":4,"sheet_y":33,"skin_variations":{}},{"unified":"1F1FC-1F1F8","sheet_x":4,"sheet_y":34,"skin_variations":{}},{"unified":"1F1FD-1F1F0","sheet_x":4,"sheet_y":35,"skin_variations":{}},{"unified":"1F1FE-1F1EA","sheet_x":4,"sheet_y":36,"skin_variations":{}},{"unified":"1F1FE-1F1F9","sheet_x":4,"sheet_y":37,"skin_variations":{}},{"unified":"1F1FF-1F1E6","sheet_x":4,"sheet_y":38,"skin_variations":{}},{"unified":"1F1FF-1F1F2","sheet_x":4,"sheet_y":39,"skin_variations":{}},{"unified":"1F1FF-1F1FC","sheet_x":4,"sheet_y":40,"skin_variations":{}},{"unified":"1F201","sheet_x":4,"sheet_y":41,"skin_variations":{}},{"unified":"1F202-FE0F","sheet_x":4,"sheet_y":42,"skin_variations":{}},{"unified":"1F21A","sheet_x":4,"sheet_y":43,"skin_variations":{}},{"unified":"1F22F","sheet_x":4,"sheet_y":44,"skin_variations":{}},{"unified":"1F232","sheet_x":4,"sheet_y":45,"skin_variations":{}},{"unified":"1F233","sheet_x":4,"sheet_y":46,"skin_variations":{}},{"unified":"1F234","sheet_x":4,"sheet_y":47,"skin_variations":{}},{"unified":"1F235","sheet_x":4,"sheet_y":48,"skin_variations":{}},{"unified":"1F236","sheet_x":4,"sheet_y":49,"skin_variations":{}},{"unified":"1F237-FE0F","sheet_x":4,"sheet_y":50,"skin_variations":{}},{"unified":"1F238","sheet_x":4,"sheet_y":51,"skin_variations":{}},{"unified":"1F239","sheet_x":4,"sheet_y":52,"skin_variations":{}},{"unified":"1F23A","sheet_x":4,"sheet_y":53,"skin_variations":{}},{"unified":"1F250","sheet_x":4,"sheet_y":54,"skin_variations":{}},{"unified":"1F251","sheet_x":4,"sheet_y":55,"skin_variations":{}},{"unified":"1F300","sheet_x":4,"sheet_y":56,"skin_variations":{}},{"unified":"1F301","sheet_x":4,"sheet_y":57,"skin_variations":{}},{"unified":"1F302","sheet_x":4,"sheet_y":58,"skin_variations":{}},{"unified":"1F303","sheet_x":4,"sheet_y":59,"skin_variations":{}},{"unified":"1F304","sheet_x":4,"sheet_y":60,"skin_variations":{}},{"unified":"1F305","sheet_x":4,"sheet_y":61,"skin_variations":{}},{"unified":"1F306","sheet_x":5,"sheet_y":0,"skin_variations":{}},{"unified":"1F307","sheet_x":5,"sheet_y":1,"skin_variations":{}},{"unified":"1F308","sheet_x":5,"sheet_y":2,"skin_variations":{}},{"unified":"1F309","sheet_x":5,"sheet_y":3,"skin_variations":{}},{"unified":"1F30A","sheet_x":5,"sheet_y":4,"skin_variations":{}},{"unified":"1F30B","sheet_x":5,"sheet_y":5,"skin_variations":{}},{"unified":"1F30C","sheet_x":5,"sheet_y":6,"skin_variations":{}},{"unified":"1F30D","sheet_x":5,"sheet_y":7,"skin_variations":{}},{"unified":"1F30E","sheet_x":5,"sheet_y":8,"skin_variations":{}},{"unified":"1F30F","sheet_x":5,"sheet_y":9,"skin_variations":{}},{"unified":"1F310","sheet_x":5,"sheet_y":10,"skin_variations":{}},{"unified":"1F311","sheet_x":5,"sheet_y":11,"skin_variations":{}},{"unified":"1F312","sheet_x":5,"sheet_y":12,"skin_variations":{}},{"unified":"1F313","sheet_x":5,"sheet_y":13,"skin_variations":{}},{"unified":"1F314","sheet_x":5,"sheet_y":14,"skin_variations":{}},{"unified":"1F315","sheet_x":5,"sheet_y":15,"skin_variations":{}},{"unified":"1F316","sheet_x":5,"sheet_y":16,"skin_variations":{}},{"unified":"1F317","sheet_x":5,"sheet_y":17,"skin_variations":{}},{"unified":"1F318","sheet_x":5,"sheet_y":18,"skin_variations":{}},{"unified":"1F319","sheet_x":5,"sheet_y":19,"skin_variations":{}},{"unified":"1F31A","sheet_x":5,"sheet_y":20,"skin_variations":{}},{"unified":"1F31B","sheet_x":5,"sheet_y":21,"skin_variations":{}},{"unified":"1F31C","sheet_x":5,"sheet_y":22,"skin_variations":{}},{"unified":"1F31D","sheet_x":5,"sheet_y":23,"skin_variations":{}},{"unified":"1F31E","sheet_x":5,"sheet_y":24,"skin_variations":{}},{"unified":"1F31F","sheet_x":5,"sheet_y":25,"skin_variations":{}},{"unified":"1F320","sheet_x":5,"sheet_y":26,"skin_variations":{}},{"unified":"1F321-FE0F","sheet_x":5,"sheet_y":27,"skin_variations":{}},{"unified":"1F324-FE0F","sheet_x":5,"sheet_y":28,"skin_variations":{}},{"unified":"1F325-FE0F","sheet_x":5,"sheet_y":29,"skin_variations":{}},{"unified":"1F326-FE0F","sheet_x":5,"sheet_y":30,"skin_variations":{}},{"unified":"1F327-FE0F","sheet_x":5,"sheet_y":31,"skin_variations":{}},{"unified":"1F328-FE0F","sheet_x":5,"sheet_y":32,"skin_variations":{}},{"unified":"1F329-FE0F","sheet_x":5,"sheet_y":33,"skin_variations":{}},{"unified":"1F32A-FE0F","sheet_x":5,"sheet_y":34,"skin_variations":{}},{"unified":"1F32B-FE0F","sheet_x":5,"sheet_y":35,"skin_variations":{}},{"unified":"1F32C-FE0F","sheet_x":5,"sheet_y":36,"skin_variations":{}},{"unified":"1F32D","sheet_x":5,"sheet_y":37,"skin_variations":{}},{"unified":"1F32E","sheet_x":5,"sheet_y":38,"skin_variations":{}},{"unified":"1F32F","sheet_x":5,"sheet_y":39,"skin_variations":{}},{"unified":"1F330","sheet_x":5,"sheet_y":40,"skin_variations":{}},{"unified":"1F331","sheet_x":5,"sheet_y":41,"skin_variations":{}},{"unified":"1F332","sheet_x":5,"sheet_y":42,"skin_variations":{}},{"unified":"1F333","sheet_x":5,"sheet_y":43,"skin_variations":{}},{"unified":"1F334","sheet_x":5,"sheet_y":44,"skin_variations":{}},{"unified":"1F335","sheet_x":5,"sheet_y":45,"skin_variations":{}},{"unified":"1F336-FE0F","sheet_x":5,"sheet_y":46,"skin_variations":{}},{"unified":"1F337","sheet_x":5,"sheet_y":47,"skin_variations":{}},{"unified":"1F338","sheet_x":5,"sheet_y":48,"skin_variations":{}},{"unified":"1F339","sheet_x":5,"sheet_y":49,"skin_variations":{}},{"unified":"1F33A","sheet_x":5,"sheet_y":50,"skin_variations":{}},{"unified":"1F33B","sheet_x":5,"sheet_y":51,"skin_variations":{}},{"unified":"1F33C","sheet_x":5,"sheet_y":52,"skin_variations":{}},{"unified":"1F33D","sheet_x":5,"sheet_y":53,"skin_variations":{}},{"unified":"1F33E","sheet_x":5,"sheet_y":54,"skin_variations":{}},{"unified":"1F33F","sheet_x":5,"sheet_y":55,"skin_variations":{}},{"unified":"1F340","sheet_x":5,"sheet_y":56,"skin_variations":{}},{"unified":"1F341","sheet_x":5,"sheet_y":57,"skin_variations":{}},{"unified":"1F342","sheet_x":5,"sheet_y":58,"skin_variations":{}},{"unified":"1F343","sheet_x":5,"sheet_y":59,"skin_variations":{}},{"unified":"1F344-200D-1F7EB","sheet_x":5,"sheet_y":60,"skin_variations":{}},{"unified":"1F344","sheet_x":5,"sheet_y":61,"skin_variations":{}},{"unified":"1F345","sheet_x":6,"sheet_y":0,"skin_variations":{}},{"unified":"1F346","sheet_x":6,"sheet_y":1,"skin_variations":{}},{"unified":"1F347","sheet_x":6,"sheet_y":2,"skin_variations":{}},{"unified":"1F348","sheet_x":6,"sheet_y":3,"skin_variations":{}},{"unified":"1F349","sheet_x":6,"sheet_y":4,"skin_variations":{}},{"unified":"1F34A","sheet_x":6,"sheet_y":5,"skin_variations":{}},{"unified":"1F34B-200D-1F7E9","sheet_x":6,"sheet_y":6,"skin_variations":{}},{"unified":"1F34B","sheet_x":6,"sheet_y":7,"skin_variations":{}},{"unified":"1F34C","sheet_x":6,"sheet_y":8,"skin_variations":{}},{"unified":"1F34D","sheet_x":6,"sheet_y":9,"skin_variations":{}},{"unified":"1F34E","sheet_x":6,"sheet_y":10,"skin_variations":{}},{"unified":"1F34F","sheet_x":6,"sheet_y":11,"skin_variations":{}},{"unified":"1F350","sheet_x":6,"sheet_y":12,"skin_variations":{}},{"unified":"1F351","sheet_x":6,"sheet_y":13,"skin_variations":{}},{"unified":"1F352","sheet_x":6,"sheet_y":14,"skin_variations":{}},{"unified":"1F353","sheet_x":6,"sheet_y":15,"skin_variations":{}},{"unified":"1F354","sheet_x":6,"sheet_y":16,"skin_variations":{}},{"unified":"1F355","sheet_x":6,"sheet_y":17,"skin_variations":{}},{"unified":"1F356","sheet_x":6,"sheet_y":18,"skin_variations":{}},{"unified":"1F357","sheet_x":6,"sheet_y":19,"skin_variations":{}},{"unified":"1F358","sheet_x":6,"sheet_y":20,"skin_variations":{}},{"unified":"1F359","sheet_x":6,"sheet_y":21,"skin_variations":{}},{"unified":"1F35A","sheet_x":6,"sheet_y":22,"skin_variations":{}},{"unified":"1F35B","sheet_x":6,"sheet_y":23,"skin_variations":{}},{"unified":"1F35C","sheet_x":6,"sheet_y":24,"skin_variations":{}},{"unified":"1F35D","sheet_x":6,"sheet_y":25,"skin_variations":{}},{"unified":"1F35E","sheet_x":6,"sheet_y":26,"skin_variations":{}},{"unified":"1F35F","sheet_x":6,"sheet_y":27,"skin_variations":{}},{"unified":"1F360","sheet_x":6,"sheet_y":28,"skin_variations":{}},{"unified":"1F361","sheet_x":6,"sheet_y":29,"skin_variations":{}},{"unified":"1F362","sheet_x":6,"sheet_y":30,"skin_variations":{}},{"unified":"1F363","sheet_x":6,"sheet_y":31,"skin_variations":{}},{"unified":"1F364","sheet_x":6,"sheet_y":32,"skin_variations":{}},{"unified":"1F365","sheet_x":6,"sheet_y":33,"skin_variations":{}},{"unified":"1F366","sheet_x":6,"sheet_y":34,"skin_variations":{}},{"unified":"1F367","sheet_x":6,"sheet_y":35,"skin_variations":{}},{"unified":"1F368","sheet_x":6,"sheet_y":36,"skin_variations":{}},{"unified":"1F369","sheet_x":6,"sheet_y":37,"skin_variations":{}},{"unified":"1F36A","sheet_x":6,"sheet_y":38,"skin_variations":{}},{"unified":"1F36B","sheet_x":6,"sheet_y":39,"skin_variations":{}},{"unified":"1F36C","sheet_x":6,"sheet_y":40,"skin_variations":{}},{"unified":"1F36D","sheet_x":6,"sheet_y":41,"skin_variations":{}},{"unified":"1F36E","sheet_x":6,"sheet_y":42,"skin_variations":{}},{"unified":"1F36F","sheet_x":6,"sheet_y":43,"skin_variations":{}},{"unified":"1F370","sheet_x":6,"sheet_y":44,"skin_variations":{}},{"unified":"1F371","sheet_x":6,"sheet_y":45,"skin_variations":{}},{"unified":"1F372","sheet_x":6,"sheet_y":46,"skin_variations":{}},{"unified":"1F373","sheet_x":6,"sheet_y":47,"skin_variations":{}},{"unified":"1F374","sheet_x":6,"sheet_y":48,"skin_variations":{}},{"unified":"1F375","sheet_x":6,"sheet_y":49,"skin_variations":{}},{"unified":"1F376","sheet_x":6,"sheet_y":50,"skin_variations":{}},{"unified":"1F377","sheet_x":6,"sheet_y":51,"skin_variations":{}},{"unified":"1F378","sheet_x":6,"sheet_y":52,"skin_variations":{}},{"unified":"1F379","sheet_x":6,"sheet_y":53,"skin_variations":{}},{"unified":"1F37A","sheet_x":6,"sheet_y":54,"skin_variations":{}},{"unified":"1F37B","sheet_x":6,"sheet_y":55,"skin_variations":{}},{"unified":"1F37C","sheet_x":6,"sheet_y":56,"skin_variations":{}},{"unified":"1F37D-FE0F","sheet_x":6,"sheet_y":57,"skin_variations":{}},{"unified":"1F37E","sheet_x":6,"sheet_y":58,"skin_variations":{}},{"unified":"1F37F","sheet_x":6,"sheet_y":59,"skin_variations":{}},{"unified":"1F380","sheet_x":6,"sheet_y":60,"skin_variations":{}},{"unified":"1F381","sheet_x":6,"sheet_y":61,"skin_variations":{}},{"unified":"1F382","sheet_x":7,"sheet_y":0,"skin_variations":{}},{"unified":"1F383","sheet_x":7,"sheet_y":1,"skin_variations":{}},{"unified":"1F384","sheet_x":7,"sheet_y":2,"skin_variations":{}},{"unified":"1F385","sheet_x":7,"sheet_y":3,"skin_variations":{"1F3FB":{"unified":"1F385-1F3FB","sheet_x":7,"sheet_y":4},"1F3FC":{"unified":"1F385-1F3FC","sheet_x":7,"sheet_y":5},"1F3FD":{"unified":"1F385-1F3FD","sheet_x":7,"sheet_y":6},"1F3FE":{"unified":"1F385-1F3FE","sheet_x":7,"sheet_y":7},"1F3FF":{"unified":"1F385-1F3FF","sheet_x":7,"sheet_y":8}}},{"unified":"1F386","sheet_x":7,"sheet_y":9,"skin_variations":{}},{"unified":"1F387","sheet_x":7,"sheet_y":10,"skin_variations":{}},{"unified":"1F388","sheet_x":7,"sheet_y":11,"skin_variations":{}},{"unified":"1F389","sheet_x":7,"sheet_y":12,"skin_variations":{}},{"unified":"1F38A","sheet_x":7,"sheet_y":13,"skin_variations":{}},{"unified":"1F38B","sheet_x":7,"sheet_y":14,"skin_variations":{}},{"unified":"1F38C","sheet_x":7,"sheet_y":15,"skin_variations":{}},{"unified":"1F38D","sheet_x":7,"sheet_y":16,"skin_variations":{}},{"unified":"1F38E","sheet_x":7,"sheet_y":17,"skin_variations":{}},{"unified":"1F38F","sheet_x":7,"sheet_y":18,"skin_variations":{}},{"unified":"1F390","sheet_x":7,"sheet_y":19,"skin_variations":{}},{"unified":"1F391","sheet_x":7,"sheet_y":20,"skin_variations":{}},{"unified":"1F392","sheet_x":7,"sheet_y":21,"skin_variations":{}},{"unified":"1F393","sheet_x":7,"sheet_y":22,"skin_variations":{}},{"unified":"1F396-FE0F","sheet_x":7,"sheet_y":23,"skin_variations":{}},{"unified":"1F397-FE0F","sheet_x":7,"sheet_y":24,"skin_variations":{}},{"unified":"1F399-FE0F","sheet_x":7,"sheet_y":25,"skin_variations":{}},{"unified":"1F39A-FE0F","sheet_x":7,"sheet_y":26,"skin_variations":{}},{"unified":"1F39B-FE0F","sheet_x":7,"sheet_y":27,"skin_variations":{}},{"unified":"1F39E-FE0F","sheet_x":7,"sheet_y":28,"skin_variations":{}},{"unified":"1F39F-FE0F","sheet_x":7,"sheet_y":29,"skin_variations":{}},{"unified":"1F3A0","sheet_x":7,"sheet_y":30,"skin_variations":{}},{"unified":"1F3A1","sheet_x":7,"sheet_y":31,"skin_variations":{}},{"unified":"1F3A2","sheet_x":7,"sheet_y":32,"skin_variations":{}},{"unified":"1F3A3","sheet_x":7,"sheet_y":33,"skin_variations":{}},{"unified":"1F3A4","sheet_x":7,"sheet_y":34,"skin_variations":{}},{"unified":"1F3A5","sheet_x":7,"sheet_y":35,"skin_variations":{}},{"unified":"1F3A6","sheet_x":7,"sheet_y":36,"skin_variations":{}},{"unified":"1F3A7","sheet_x":7,"sheet_y":37,"skin_variations":{}},{"unified":"1F3A8","sheet_x":7,"sheet_y":38,"skin_variations":{}},{"unified":"1F3A9","sheet_x":7,"sheet_y":39,"skin_variations":{}},{"unified":"1F3AA","sheet_x":7,"sheet_y":40,"skin_variations":{}},{"unified":"1F3AB","sheet_x":7,"sheet_y":41,"skin_variations":{}},{"unified":"1F3AC","sheet_x":7,"sheet_y":42,"skin_variations":{}},{"unified":"1F3AD","sheet_x":7,"sheet_y":43,"skin_variations":{}},{"unified":"1F3AE","sheet_x":7,"sheet_y":44,"skin_variations":{}},{"unified":"1F3AF","sheet_x":7,"sheet_y":45,"skin_variations":{}},{"unified":"1F3B0","sheet_x":7,"sheet_y":46,"skin_variations":{}},{"unified":"1F3B1","sheet_x":7,"sheet_y":47,"skin_variations":{}},{"unified":"1F3B2","sheet_x":7,"sheet_y":48,"skin_variations":{}},{"unified":"1F3B3","sheet_x":7,"sheet_y":49,"skin_variations":{}},{"unified":"1F3B4","sheet_x":7,"sheet_y":50,"skin_variations":{}},{"unified":"1F3B5","sheet_x":7,"sheet_y":51,"skin_variations":{}},{"unified":"1F3B6","sheet_x":7,"sheet_y":52,"skin_variations":{}},{"unified":"1F3B7","sheet_x":7,"sheet_y":53,"skin_variations":{}},{"unified":"1F3B8","sheet_x":7,"sheet_y":54,"skin_variations":{}},{"unified":"1F3B9","sheet_x":7,"sheet_y":55,"skin_variations":{}},{"unified":"1F3BA","sheet_x":7,"sheet_y":56,"skin_variations":{}},{"unified":"1F3BB","sheet_x":7,"sheet_y":57,"skin_variations":{}},{"unified":"1F3BC","sheet_x":7,"sheet_y":58,"skin_variations":{}},{"unified":"1F3BD","sheet_x":7,"sheet_y":59,"skin_variations":{}},{"unified":"1F3BE","sheet_x":7,"sheet_y":60,"skin_variations":{}},{"unified":"1F3BF","sheet_x":7,"sheet_y":61,"skin_variations":{}},{"unified":"1F3C0","sheet_x":8,"sheet_y":0,"skin_variations":{}},{"unified":"1F3C1","sheet_x":8,"sheet_y":1,"skin_variations":{}},{"unified":"1F3C2","sheet_x":8,"sheet_y":2,"skin_variations":{"1F3FB":{"unified":"1F3C2-1F3FB","sheet_x":8,"sheet_y":3},"1F3FC":{"unified":"1F3C2-1F3FC","sheet_x":8,"sheet_y":4},"1F3FD":{"unified":"1F3C2-1F3FD","sheet_x":8,"sheet_y":5},"1F3FE":{"unified":"1F3C2-1F3FE","sheet_x":8,"sheet_y":6},"1F3FF":{"unified":"1F3C2-1F3FF","sheet_x":8,"sheet_y":7}}},{"unified":"1F3C3-200D-2640-FE0F","sheet_x":8,"sheet_y":8,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2640-FE0F","sheet_x":8,"sheet_y":9},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2640-FE0F","sheet_x":8,"sheet_y":10},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2640-FE0F","sheet_x":8,"sheet_y":11},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2640-FE0F","sheet_x":8,"sheet_y":12},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2640-FE0F","sheet_x":8,"sheet_y":13}}},{"unified":"1F3C3-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":14,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":15},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":16},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":17},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":18},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":19}}},{"unified":"1F3C3-200D-2642-FE0F","sheet_x":8,"sheet_y":20,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2642-FE0F","sheet_x":8,"sheet_y":21},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2642-FE0F","sheet_x":8,"sheet_y":22},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2642-FE0F","sheet_x":8,"sheet_y":23},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2642-FE0F","sheet_x":8,"sheet_y":24},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2642-FE0F","sheet_x":8,"sheet_y":25}}},{"unified":"1F3C3-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":26,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":27},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":28},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":29},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":30},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":8,"sheet_y":31}}},{"unified":"1F3C3-200D-27A1-FE0F","sheet_x":8,"sheet_y":32,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-27A1-FE0F","sheet_x":8,"sheet_y":33},"1F3FC":{"unified":"1F3C3-1F3FC-200D-27A1-FE0F","sheet_x":8,"sheet_y":34},"1F3FD":{"unified":"1F3C3-1F3FD-200D-27A1-FE0F","sheet_x":8,"sheet_y":35},"1F3FE":{"unified":"1F3C3-1F3FE-200D-27A1-FE0F","sheet_x":8,"sheet_y":36},"1F3FF":{"unified":"1F3C3-1F3FF-200D-27A1-FE0F","sheet_x":8,"sheet_y":37}}},{"unified":"1F3C3","sheet_x":8,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB","sheet_x":8,"sheet_y":39},"1F3FC":{"unified":"1F3C3-1F3FC","sheet_x":8,"sheet_y":40},"1F3FD":{"unified":"1F3C3-1F3FD","sheet_x":8,"sheet_y":41},"1F3FE":{"unified":"1F3C3-1F3FE","sheet_x":8,"sheet_y":42},"1F3FF":{"unified":"1F3C3-1F3FF","sheet_x":8,"sheet_y":43}}},{"unified":"1F3C4-200D-2640-FE0F","sheet_x":8,"sheet_y":44,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB-200D-2640-FE0F","sheet_x":8,"sheet_y":45},"1F3FC":{"unified":"1F3C4-1F3FC-200D-2640-FE0F","sheet_x":8,"sheet_y":46},"1F3FD":{"unified":"1F3C4-1F3FD-200D-2640-FE0F","sheet_x":8,"sheet_y":47},"1F3FE":{"unified":"1F3C4-1F3FE-200D-2640-FE0F","sheet_x":8,"sheet_y":48},"1F3FF":{"unified":"1F3C4-1F3FF-200D-2640-FE0F","sheet_x":8,"sheet_y":49}}},{"unified":"1F3C4-200D-2642-FE0F","sheet_x":8,"sheet_y":50,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB-200D-2642-FE0F","sheet_x":8,"sheet_y":51},"1F3FC":{"unified":"1F3C4-1F3FC-200D-2642-FE0F","sheet_x":8,"sheet_y":52},"1F3FD":{"unified":"1F3C4-1F3FD-200D-2642-FE0F","sheet_x":8,"sheet_y":53},"1F3FE":{"unified":"1F3C4-1F3FE-200D-2642-FE0F","sheet_x":8,"sheet_y":54},"1F3FF":{"unified":"1F3C4-1F3FF-200D-2642-FE0F","sheet_x":8,"sheet_y":55}}},{"unified":"1F3C4","sheet_x":8,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB","sheet_x":8,"sheet_y":57},"1F3FC":{"unified":"1F3C4-1F3FC","sheet_x":8,"sheet_y":58},"1F3FD":{"unified":"1F3C4-1F3FD","sheet_x":8,"sheet_y":59},"1F3FE":{"unified":"1F3C4-1F3FE","sheet_x":8,"sheet_y":60},"1F3FF":{"unified":"1F3C4-1F3FF","sheet_x":8,"sheet_y":61}}},{"unified":"1F3C5","sheet_x":9,"sheet_y":0,"skin_variations":{}},{"unified":"1F3C6","sheet_x":9,"sheet_y":1,"skin_variations":{}},{"unified":"1F3C7","sheet_x":9,"sheet_y":2,"skin_variations":{"1F3FB":{"unified":"1F3C7-1F3FB","sheet_x":9,"sheet_y":3},"1F3FC":{"unified":"1F3C7-1F3FC","sheet_x":9,"sheet_y":4},"1F3FD":{"unified":"1F3C7-1F3FD","sheet_x":9,"sheet_y":5},"1F3FE":{"unified":"1F3C7-1F3FE","sheet_x":9,"sheet_y":6},"1F3FF":{"unified":"1F3C7-1F3FF","sheet_x":9,"sheet_y":7}}},{"unified":"1F3C8","sheet_x":9,"sheet_y":8,"skin_variations":{}},{"unified":"1F3C9","sheet_x":9,"sheet_y":9,"skin_variations":{}},{"unified":"1F3CA-200D-2640-FE0F","sheet_x":9,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB-200D-2640-FE0F","sheet_x":9,"sheet_y":11},"1F3FC":{"unified":"1F3CA-1F3FC-200D-2640-FE0F","sheet_x":9,"sheet_y":12},"1F3FD":{"unified":"1F3CA-1F3FD-200D-2640-FE0F","sheet_x":9,"sheet_y":13},"1F3FE":{"unified":"1F3CA-1F3FE-200D-2640-FE0F","sheet_x":9,"sheet_y":14},"1F3FF":{"unified":"1F3CA-1F3FF-200D-2640-FE0F","sheet_x":9,"sheet_y":15}}},{"unified":"1F3CA-200D-2642-FE0F","sheet_x":9,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB-200D-2642-FE0F","sheet_x":9,"sheet_y":17},"1F3FC":{"unified":"1F3CA-1F3FC-200D-2642-FE0F","sheet_x":9,"sheet_y":18},"1F3FD":{"unified":"1F3CA-1F3FD-200D-2642-FE0F","sheet_x":9,"sheet_y":19},"1F3FE":{"unified":"1F3CA-1F3FE-200D-2642-FE0F","sheet_x":9,"sheet_y":20},"1F3FF":{"unified":"1F3CA-1F3FF-200D-2642-FE0F","sheet_x":9,"sheet_y":21}}},{"unified":"1F3CA","sheet_x":9,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB","sheet_x":9,"sheet_y":23},"1F3FC":{"unified":"1F3CA-1F3FC","sheet_x":9,"sheet_y":24},"1F3FD":{"unified":"1F3CA-1F3FD","sheet_x":9,"sheet_y":25},"1F3FE":{"unified":"1F3CA-1F3FE","sheet_x":9,"sheet_y":26},"1F3FF":{"unified":"1F3CA-1F3FF","sheet_x":9,"sheet_y":27}}},{"unified":"1F3CB-FE0F-200D-2640-FE0F","sheet_x":9,"sheet_y":28,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB-200D-2640-FE0F","sheet_x":9,"sheet_y":29},"1F3FC":{"unified":"1F3CB-1F3FC-200D-2640-FE0F","sheet_x":9,"sheet_y":30},"1F3FD":{"unified":"1F3CB-1F3FD-200D-2640-FE0F","sheet_x":9,"sheet_y":31},"1F3FE":{"unified":"1F3CB-1F3FE-200D-2640-FE0F","sheet_x":9,"sheet_y":32},"1F3FF":{"unified":"1F3CB-1F3FF-200D-2640-FE0F","sheet_x":9,"sheet_y":33}}},{"unified":"1F3CB-FE0F-200D-2642-FE0F","sheet_x":9,"sheet_y":34,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB-200D-2642-FE0F","sheet_x":9,"sheet_y":35},"1F3FC":{"unified":"1F3CB-1F3FC-200D-2642-FE0F","sheet_x":9,"sheet_y":36},"1F3FD":{"unified":"1F3CB-1F3FD-200D-2642-FE0F","sheet_x":9,"sheet_y":37},"1F3FE":{"unified":"1F3CB-1F3FE-200D-2642-FE0F","sheet_x":9,"sheet_y":38},"1F3FF":{"unified":"1F3CB-1F3FF-200D-2642-FE0F","sheet_x":9,"sheet_y":39}}},{"unified":"1F3CB-FE0F","sheet_x":9,"sheet_y":40,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB","sheet_x":9,"sheet_y":41},"1F3FC":{"unified":"1F3CB-1F3FC","sheet_x":9,"sheet_y":42},"1F3FD":{"unified":"1F3CB-1F3FD","sheet_x":9,"sheet_y":43},"1F3FE":{"unified":"1F3CB-1F3FE","sheet_x":9,"sheet_y":44},"1F3FF":{"unified":"1F3CB-1F3FF","sheet_x":9,"sheet_y":45}}},{"unified":"1F3CC-FE0F-200D-2640-FE0F","sheet_x":9,"sheet_y":46,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB-200D-2640-FE0F","sheet_x":9,"sheet_y":47},"1F3FC":{"unified":"1F3CC-1F3FC-200D-2640-FE0F","sheet_x":9,"sheet_y":48},"1F3FD":{"unified":"1F3CC-1F3FD-200D-2640-FE0F","sheet_x":9,"sheet_y":49},"1F3FE":{"unified":"1F3CC-1F3FE-200D-2640-FE0F","sheet_x":9,"sheet_y":50},"1F3FF":{"unified":"1F3CC-1F3FF-200D-2640-FE0F","sheet_x":9,"sheet_y":51}}},{"unified":"1F3CC-FE0F-200D-2642-FE0F","sheet_x":9,"sheet_y":52,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB-200D-2642-FE0F","sheet_x":9,"sheet_y":53},"1F3FC":{"unified":"1F3CC-1F3FC-200D-2642-FE0F","sheet_x":9,"sheet_y":54},"1F3FD":{"unified":"1F3CC-1F3FD-200D-2642-FE0F","sheet_x":9,"sheet_y":55},"1F3FE":{"unified":"1F3CC-1F3FE-200D-2642-FE0F","sheet_x":9,"sheet_y":56},"1F3FF":{"unified":"1F3CC-1F3FF-200D-2642-FE0F","sheet_x":9,"sheet_y":57}}},{"unified":"1F3CC-FE0F","sheet_x":9,"sheet_y":58,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB","sheet_x":9,"sheet_y":59},"1F3FC":{"unified":"1F3CC-1F3FC","sheet_x":9,"sheet_y":60},"1F3FD":{"unified":"1F3CC-1F3FD","sheet_x":9,"sheet_y":61},"1F3FE":{"unified":"1F3CC-1F3FE","sheet_x":10,"sheet_y":0},"1F3FF":{"unified":"1F3CC-1F3FF","sheet_x":10,"sheet_y":1}}},{"unified":"1F3CD-FE0F","sheet_x":10,"sheet_y":2,"skin_variations":{}},{"unified":"1F3CE-FE0F","sheet_x":10,"sheet_y":3,"skin_variations":{}},{"unified":"1F3CF","sheet_x":10,"sheet_y":4,"skin_variations":{}},{"unified":"1F3D0","sheet_x":10,"sheet_y":5,"skin_variations":{}},{"unified":"1F3D1","sheet_x":10,"sheet_y":6,"skin_variations":{}},{"unified":"1F3D2","sheet_x":10,"sheet_y":7,"skin_variations":{}},{"unified":"1F3D3","sheet_x":10,"sheet_y":8,"skin_variations":{}},{"unified":"1F3D4-FE0F","sheet_x":10,"sheet_y":9,"skin_variations":{}},{"unified":"1F3D5-FE0F","sheet_x":10,"sheet_y":10,"skin_variations":{}},{"unified":"1F3D6-FE0F","sheet_x":10,"sheet_y":11,"skin_variations":{}},{"unified":"1F3D7-FE0F","sheet_x":10,"sheet_y":12,"skin_variations":{}},{"unified":"1F3D8-FE0F","sheet_x":10,"sheet_y":13,"skin_variations":{}},{"unified":"1F3D9-FE0F","sheet_x":10,"sheet_y":14,"skin_variations":{}},{"unified":"1F3DA-FE0F","sheet_x":10,"sheet_y":15,"skin_variations":{}},{"unified":"1F3DB-FE0F","sheet_x":10,"sheet_y":16,"skin_variations":{}},{"unified":"1F3DC-FE0F","sheet_x":10,"sheet_y":17,"skin_variations":{}},{"unified":"1F3DD-FE0F","sheet_x":10,"sheet_y":18,"skin_variations":{}},{"unified":"1F3DE-FE0F","sheet_x":10,"sheet_y":19,"skin_variations":{}},{"unified":"1F3DF-FE0F","sheet_x":10,"sheet_y":20,"skin_variations":{}},{"unified":"1F3E0","sheet_x":10,"sheet_y":21,"skin_variations":{}},{"unified":"1F3E1","sheet_x":10,"sheet_y":22,"skin_variations":{}},{"unified":"1F3E2","sheet_x":10,"sheet_y":23,"skin_variations":{}},{"unified":"1F3E3","sheet_x":10,"sheet_y":24,"skin_variations":{}},{"unified":"1F3E4","sheet_x":10,"sheet_y":25,"skin_variations":{}},{"unified":"1F3E5","sheet_x":10,"sheet_y":26,"skin_variations":{}},{"unified":"1F3E6","sheet_x":10,"sheet_y":27,"skin_variations":{}},{"unified":"1F3E7","sheet_x":10,"sheet_y":28,"skin_variations":{}},{"unified":"1F3E8","sheet_x":10,"sheet_y":29,"skin_variations":{}},{"unified":"1F3E9","sheet_x":10,"sheet_y":30,"skin_variations":{}},{"unified":"1F3EA","sheet_x":10,"sheet_y":31,"skin_variations":{}},{"unified":"1F3EB","sheet_x":10,"sheet_y":32,"skin_variations":{}},{"unified":"1F3EC","sheet_x":10,"sheet_y":33,"skin_variations":{}},{"unified":"1F3ED","sheet_x":10,"sheet_y":34,"skin_variations":{}},{"unified":"1F3EE","sheet_x":10,"sheet_y":35,"skin_variations":{}},{"unified":"1F3EF","sheet_x":10,"sheet_y":36,"skin_variations":{}},{"unified":"1F3F0","sheet_x":10,"sheet_y":37,"skin_variations":{}},{"unified":"1F3F3-FE0F-200D-1F308","sheet_x":10,"sheet_y":38,"skin_variations":{}},{"unified":"1F3F3-FE0F-200D-26A7-FE0F","sheet_x":10,"sheet_y":39,"skin_variations":{}},{"unified":"1F3F3-FE0F","sheet_x":10,"sheet_y":40,"skin_variations":{}},{"unified":"1F3F4-200D-2620-FE0F","sheet_x":10,"sheet_y":41,"skin_variations":{}},{"unified":"1F3F4-E0067-E0062-E0065-E006E-E0067-E007F","sheet_x":10,"sheet_y":42,"skin_variations":{}},{"unified":"1F3F4-E0067-E0062-E0073-E0063-E0074-E007F","sheet_x":10,"sheet_y":43,"skin_variations":{}},{"unified":"1F3F4-E0067-E0062-E0077-E006C-E0073-E007F","sheet_x":10,"sheet_y":44,"skin_variations":{}},{"unified":"1F3F4","sheet_x":10,"sheet_y":45,"skin_variations":{}},{"unified":"1F3F5-FE0F","sheet_x":10,"sheet_y":46,"skin_variations":{}},{"unified":"1F3F7-FE0F","sheet_x":10,"sheet_y":47,"skin_variations":{}},{"unified":"1F3F8","sheet_x":10,"sheet_y":48,"skin_variations":{}},{"unified":"1F3F9","sheet_x":10,"sheet_y":49,"skin_variations":{}},{"unified":"1F3FA","sheet_x":10,"sheet_y":50,"skin_variations":{}},{"unified":"1F3FB","sheet_x":10,"sheet_y":51,"skin_variations":{}},{"unified":"1F3FC","sheet_x":10,"sheet_y":52,"skin_variations":{}},{"unified":"1F3FD","sheet_x":10,"sheet_y":53,"skin_variations":{}},{"unified":"1F3FE","sheet_x":10,"sheet_y":54,"skin_variations":{}},{"unified":"1F3FF","sheet_x":10,"sheet_y":55,"skin_variations":{}},{"unified":"1F400","sheet_x":10,"sheet_y":56,"skin_variations":{}},{"unified":"1F401","sheet_x":10,"sheet_y":57,"skin_variations":{}},{"unified":"1F402","sheet_x":10,"sheet_y":58,"skin_variations":{}},{"unified":"1F403","sheet_x":10,"sheet_y":59,"skin_variations":{}},{"unified":"1F404","sheet_x":10,"sheet_y":60,"skin_variations":{}},{"unified":"1F405","sheet_x":10,"sheet_y":61,"skin_variations":{}},{"unified":"1F406","sheet_x":11,"sheet_y":0,"skin_variations":{}},{"unified":"1F407","sheet_x":11,"sheet_y":1,"skin_variations":{}},{"unified":"1F408-200D-2B1B","sheet_x":11,"sheet_y":2,"skin_variations":{}},{"unified":"1F408","sheet_x":11,"sheet_y":3,"skin_variations":{}},{"unified":"1F409","sheet_x":11,"sheet_y":4,"skin_variations":{}},{"unified":"1F40A","sheet_x":11,"sheet_y":5,"skin_variations":{}},{"unified":"1F40B","sheet_x":11,"sheet_y":6,"skin_variations":{}},{"unified":"1F40C","sheet_x":11,"sheet_y":7,"skin_variations":{}},{"unified":"1F40D","sheet_x":11,"sheet_y":8,"skin_variations":{}},{"unified":"1F40E","sheet_x":11,"sheet_y":9,"skin_variations":{}},{"unified":"1F40F","sheet_x":11,"sheet_y":10,"skin_variations":{}},{"unified":"1F410","sheet_x":11,"sheet_y":11,"skin_variations":{}},{"unified":"1F411","sheet_x":11,"sheet_y":12,"skin_variations":{}},{"unified":"1F412","sheet_x":11,"sheet_y":13,"skin_variations":{}},{"unified":"1F413","sheet_x":11,"sheet_y":14,"skin_variations":{}},{"unified":"1F414","sheet_x":11,"sheet_y":15,"skin_variations":{}},{"unified":"1F415-200D-1F9BA","sheet_x":11,"sheet_y":16,"skin_variations":{}},{"unified":"1F415","sheet_x":11,"sheet_y":17,"skin_variations":{}},{"unified":"1F416","sheet_x":11,"sheet_y":18,"skin_variations":{}},{"unified":"1F417","sheet_x":11,"sheet_y":19,"skin_variations":{}},{"unified":"1F418","sheet_x":11,"sheet_y":20,"skin_variations":{}},{"unified":"1F419","sheet_x":11,"sheet_y":21,"skin_variations":{}},{"unified":"1F41A","sheet_x":11,"sheet_y":22,"skin_variations":{}},{"unified":"1F41B","sheet_x":11,"sheet_y":23,"skin_variations":{}},{"unified":"1F41C","sheet_x":11,"sheet_y":24,"skin_variations":{}},{"unified":"1F41D","sheet_x":11,"sheet_y":25,"skin_variations":{}},{"unified":"1F41E","sheet_x":11,"sheet_y":26,"skin_variations":{}},{"unified":"1F41F","sheet_x":11,"sheet_y":27,"skin_variations":{}},{"unified":"1F420","sheet_x":11,"sheet_y":28,"skin_variations":{}},{"unified":"1F421","sheet_x":11,"sheet_y":29,"skin_variations":{}},{"unified":"1F422","sheet_x":11,"sheet_y":30,"skin_variations":{}},{"unified":"1F423","sheet_x":11,"sheet_y":31,"skin_variations":{}},{"unified":"1F424","sheet_x":11,"sheet_y":32,"skin_variations":{}},{"unified":"1F425","sheet_x":11,"sheet_y":33,"skin_variations":{}},{"unified":"1F426-200D-1F525","sheet_x":11,"sheet_y":34,"skin_variations":{}},{"unified":"1F426-200D-2B1B","sheet_x":11,"sheet_y":35,"skin_variations":{}},{"unified":"1F426","sheet_x":11,"sheet_y":36,"skin_variations":{}},{"unified":"1F427","sheet_x":11,"sheet_y":37,"skin_variations":{}},{"unified":"1F428","sheet_x":11,"sheet_y":38,"skin_variations":{}},{"unified":"1F429","sheet_x":11,"sheet_y":39,"skin_variations":{}},{"unified":"1F42A","sheet_x":11,"sheet_y":40,"skin_variations":{}},{"unified":"1F42B","sheet_x":11,"sheet_y":41,"skin_variations":{}},{"unified":"1F42C","sheet_x":11,"sheet_y":42,"skin_variations":{}},{"unified":"1F42D","sheet_x":11,"sheet_y":43,"skin_variations":{}},{"unified":"1F42E","sheet_x":11,"sheet_y":44,"skin_variations":{}},{"unified":"1F42F","sheet_x":11,"sheet_y":45,"skin_variations":{}},{"unified":"1F430","sheet_x":11,"sheet_y":46,"skin_variations":{}},{"unified":"1F431","sheet_x":11,"sheet_y":47,"skin_variations":{}},{"unified":"1F432","sheet_x":11,"sheet_y":48,"skin_variations":{}},{"unified":"1F433","sheet_x":11,"sheet_y":49,"skin_variations":{}},{"unified":"1F434","sheet_x":11,"sheet_y":50,"skin_variations":{}},{"unified":"1F435","sheet_x":11,"sheet_y":51,"skin_variations":{}},{"unified":"1F436","sheet_x":11,"sheet_y":52,"skin_variations":{}},{"unified":"1F437","sheet_x":11,"sheet_y":53,"skin_variations":{}},{"unified":"1F438","sheet_x":11,"sheet_y":54,"skin_variations":{}},{"unified":"1F439","sheet_x":11,"sheet_y":55,"skin_variations":{}},{"unified":"1F43A","sheet_x":11,"sheet_y":56,"skin_variations":{}},{"unified":"1F43B-200D-2744-FE0F","sheet_x":11,"sheet_y":57,"skin_variations":{}},{"unified":"1F43B","sheet_x":11,"sheet_y":58,"skin_variations":{}},{"unified":"1F43C","sheet_x":11,"sheet_y":59,"skin_variations":{}},{"unified":"1F43D","sheet_x":11,"sheet_y":60,"skin_variations":{}},{"unified":"1F43E","sheet_x":11,"sheet_y":61,"skin_variations":{}},{"unified":"1F43F-FE0F","sheet_x":12,"sheet_y":0,"skin_variations":{}},{"unified":"1F440","sheet_x":12,"sheet_y":1,"skin_variations":{}},{"unified":"1F441-FE0F-200D-1F5E8-FE0F","sheet_x":12,"sheet_y":2,"skin_variations":{}},{"unified":"1F441-FE0F","sheet_x":12,"sheet_y":3,"skin_variations":{}},{"unified":"1F442","sheet_x":12,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1F442-1F3FB","sheet_x":12,"sheet_y":5},"1F3FC":{"unified":"1F442-1F3FC","sheet_x":12,"sheet_y":6},"1F3FD":{"unified":"1F442-1F3FD","sheet_x":12,"sheet_y":7},"1F3FE":{"unified":"1F442-1F3FE","sheet_x":12,"sheet_y":8},"1F3FF":{"unified":"1F442-1F3FF","sheet_x":12,"sheet_y":9}}},{"unified":"1F443","sheet_x":12,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F443-1F3FB","sheet_x":12,"sheet_y":11},"1F3FC":{"unified":"1F443-1F3FC","sheet_x":12,"sheet_y":12},"1F3FD":{"unified":"1F443-1F3FD","sheet_x":12,"sheet_y":13},"1F3FE":{"unified":"1F443-1F3FE","sheet_x":12,"sheet_y":14},"1F3FF":{"unified":"1F443-1F3FF","sheet_x":12,"sheet_y":15}}},{"unified":"1F444","sheet_x":12,"sheet_y":16,"skin_variations":{}},{"unified":"1F445","sheet_x":12,"sheet_y":17,"skin_variations":{}},{"unified":"1F446","sheet_x":12,"sheet_y":18,"skin_variations":{"1F3FB":{"unified":"1F446-1F3FB","sheet_x":12,"sheet_y":19},"1F3FC":{"unified":"1F446-1F3FC","sheet_x":12,"sheet_y":20},"1F3FD":{"unified":"1F446-1F3FD","sheet_x":12,"sheet_y":21},"1F3FE":{"unified":"1F446-1F3FE","sheet_x":12,"sheet_y":22},"1F3FF":{"unified":"1F446-1F3FF","sheet_x":12,"sheet_y":23}}},{"unified":"1F447","sheet_x":12,"sheet_y":24,"skin_variations":{"1F3FB":{"unified":"1F447-1F3FB","sheet_x":12,"sheet_y":25},"1F3FC":{"unified":"1F447-1F3FC","sheet_x":12,"sheet_y":26},"1F3FD":{"unified":"1F447-1F3FD","sheet_x":12,"sheet_y":27},"1F3FE":{"unified":"1F447-1F3FE","sheet_x":12,"sheet_y":28},"1F3FF":{"unified":"1F447-1F3FF","sheet_x":12,"sheet_y":29}}},{"unified":"1F448","sheet_x":12,"sheet_y":30,"skin_variations":{"1F3FB":{"unified":"1F448-1F3FB","sheet_x":12,"sheet_y":31},"1F3FC":{"unified":"1F448-1F3FC","sheet_x":12,"sheet_y":32},"1F3FD":{"unified":"1F448-1F3FD","sheet_x":12,"sheet_y":33},"1F3FE":{"unified":"1F448-1F3FE","sheet_x":12,"sheet_y":34},"1F3FF":{"unified":"1F448-1F3FF","sheet_x":12,"sheet_y":35}}},{"unified":"1F449","sheet_x":12,"sheet_y":36,"skin_variations":{"1F3FB":{"unified":"1F449-1F3FB","sheet_x":12,"sheet_y":37},"1F3FC":{"unified":"1F449-1F3FC","sheet_x":12,"sheet_y":38},"1F3FD":{"unified":"1F449-1F3FD","sheet_x":12,"sheet_y":39},"1F3FE":{"unified":"1F449-1F3FE","sheet_x":12,"sheet_y":40},"1F3FF":{"unified":"1F449-1F3FF","sheet_x":12,"sheet_y":41}}},{"unified":"1F44A","sheet_x":12,"sheet_y":42,"skin_variations":{"1F3FB":{"unified":"1F44A-1F3FB","sheet_x":12,"sheet_y":43},"1F3FC":{"unified":"1F44A-1F3FC","sheet_x":12,"sheet_y":44},"1F3FD":{"unified":"1F44A-1F3FD","sheet_x":12,"sheet_y":45},"1F3FE":{"unified":"1F44A-1F3FE","sheet_x":12,"sheet_y":46},"1F3FF":{"unified":"1F44A-1F3FF","sheet_x":12,"sheet_y":47}}},{"unified":"1F44B","sheet_x":12,"sheet_y":48,"skin_variations":{"1F3FB":{"unified":"1F44B-1F3FB","sheet_x":12,"sheet_y":49},"1F3FC":{"unified":"1F44B-1F3FC","sheet_x":12,"sheet_y":50},"1F3FD":{"unified":"1F44B-1F3FD","sheet_x":12,"sheet_y":51},"1F3FE":{"unified":"1F44B-1F3FE","sheet_x":12,"sheet_y":52},"1F3FF":{"unified":"1F44B-1F3FF","sheet_x":12,"sheet_y":53}}},{"unified":"1F44C","sheet_x":12,"sheet_y":54,"skin_variations":{"1F3FB":{"unified":"1F44C-1F3FB","sheet_x":12,"sheet_y":55},"1F3FC":{"unified":"1F44C-1F3FC","sheet_x":12,"sheet_y":56},"1F3FD":{"unified":"1F44C-1F3FD","sheet_x":12,"sheet_y":57},"1F3FE":{"unified":"1F44C-1F3FE","sheet_x":12,"sheet_y":58},"1F3FF":{"unified":"1F44C-1F3FF","sheet_x":12,"sheet_y":59}}},{"unified":"1F44D","sheet_x":12,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1F44D-1F3FB","sheet_x":12,"sheet_y":61},"1F3FC":{"unified":"1F44D-1F3FC","sheet_x":13,"sheet_y":0},"1F3FD":{"unified":"1F44D-1F3FD","sheet_x":13,"sheet_y":1},"1F3FE":{"unified":"1F44D-1F3FE","sheet_x":13,"sheet_y":2},"1F3FF":{"unified":"1F44D-1F3FF","sheet_x":13,"sheet_y":3}}},{"unified":"1F44E","sheet_x":13,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1F44E-1F3FB","sheet_x":13,"sheet_y":5},"1F3FC":{"unified":"1F44E-1F3FC","sheet_x":13,"sheet_y":6},"1F3FD":{"unified":"1F44E-1F3FD","sheet_x":13,"sheet_y":7},"1F3FE":{"unified":"1F44E-1F3FE","sheet_x":13,"sheet_y":8},"1F3FF":{"unified":"1F44E-1F3FF","sheet_x":13,"sheet_y":9}}},{"unified":"1F44F","sheet_x":13,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F44F-1F3FB","sheet_x":13,"sheet_y":11},"1F3FC":{"unified":"1F44F-1F3FC","sheet_x":13,"sheet_y":12},"1F3FD":{"unified":"1F44F-1F3FD","sheet_x":13,"sheet_y":13},"1F3FE":{"unified":"1F44F-1F3FE","sheet_x":13,"sheet_y":14},"1F3FF":{"unified":"1F44F-1F3FF","sheet_x":13,"sheet_y":15}}},{"unified":"1F450","sheet_x":13,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F450-1F3FB","sheet_x":13,"sheet_y":17},"1F3FC":{"unified":"1F450-1F3FC","sheet_x":13,"sheet_y":18},"1F3FD":{"unified":"1F450-1F3FD","sheet_x":13,"sheet_y":19},"1F3FE":{"unified":"1F450-1F3FE","sheet_x":13,"sheet_y":20},"1F3FF":{"unified":"1F450-1F3FF","sheet_x":13,"sheet_y":21}}},{"unified":"1F451","sheet_x":13,"sheet_y":22,"skin_variations":{}},{"unified":"1F452","sheet_x":13,"sheet_y":23,"skin_variations":{}},{"unified":"1F453","sheet_x":13,"sheet_y":24,"skin_variations":{}},{"unified":"1F454","sheet_x":13,"sheet_y":25,"skin_variations":{}},{"unified":"1F455","sheet_x":13,"sheet_y":26,"skin_variations":{}},{"unified":"1F456","sheet_x":13,"sheet_y":27,"skin_variations":{}},{"unified":"1F457","sheet_x":13,"sheet_y":28,"skin_variations":{}},{"unified":"1F458","sheet_x":13,"sheet_y":29,"skin_variations":{}},{"unified":"1F459","sheet_x":13,"sheet_y":30,"skin_variations":{}},{"unified":"1F45A","sheet_x":13,"sheet_y":31,"skin_variations":{}},{"unified":"1F45B","sheet_x":13,"sheet_y":32,"skin_variations":{}},{"unified":"1F45C","sheet_x":13,"sheet_y":33,"skin_variations":{}},{"unified":"1F45D","sheet_x":13,"sheet_y":34,"skin_variations":{}},{"unified":"1F45E","sheet_x":13,"sheet_y":35,"skin_variations":{}},{"unified":"1F45F","sheet_x":13,"sheet_y":36,"skin_variations":{}},{"unified":"1F460","sheet_x":13,"sheet_y":37,"skin_variations":{}},{"unified":"1F461","sheet_x":13,"sheet_y":38,"skin_variations":{}},{"unified":"1F462","sheet_x":13,"sheet_y":39,"skin_variations":{}},{"unified":"1F463","sheet_x":13,"sheet_y":40,"skin_variations":{}},{"unified":"1F464","sheet_x":13,"sheet_y":41,"skin_variations":{}},{"unified":"1F465","sheet_x":13,"sheet_y":42,"skin_variations":{}},{"unified":"1F466","sheet_x":13,"sheet_y":43,"skin_variations":{"1F3FB":{"unified":"1F466-1F3FB","sheet_x":13,"sheet_y":44},"1F3FC":{"unified":"1F466-1F3FC","sheet_x":13,"sheet_y":45},"1F3FD":{"unified":"1F466-1F3FD","sheet_x":13,"sheet_y":46},"1F3FE":{"unified":"1F466-1F3FE","sheet_x":13,"sheet_y":47},"1F3FF":{"unified":"1F466-1F3FF","sheet_x":13,"sheet_y":48}}},{"unified":"1F467","sheet_x":13,"sheet_y":49,"skin_variations":{"1F3FB":{"unified":"1F467-1F3FB","sheet_x":13,"sheet_y":50},"1F3FC":{"unified":"1F467-1F3FC","sheet_x":13,"sheet_y":51},"1F3FD":{"unified":"1F467-1F3FD","sheet_x":13,"sheet_y":52},"1F3FE":{"unified":"1F467-1F3FE","sheet_x":13,"sheet_y":53},"1F3FF":{"unified":"1F467-1F3FF","sheet_x":13,"sheet_y":54}}},{"unified":"1F468-200D-1F33E","sheet_x":13,"sheet_y":55,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F33E","sheet_x":13,"sheet_y":56},"1F3FC":{"unified":"1F468-1F3FC-200D-1F33E","sheet_x":13,"sheet_y":57},"1F3FD":{"unified":"1F468-1F3FD-200D-1F33E","sheet_x":13,"sheet_y":58},"1F3FE":{"unified":"1F468-1F3FE-200D-1F33E","sheet_x":13,"sheet_y":59},"1F3FF":{"unified":"1F468-1F3FF-200D-1F33E","sheet_x":13,"sheet_y":60}}},{"unified":"1F468-200D-1F373","sheet_x":13,"sheet_y":61,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F373","sheet_x":14,"sheet_y":0},"1F3FC":{"unified":"1F468-1F3FC-200D-1F373","sheet_x":14,"sheet_y":1},"1F3FD":{"unified":"1F468-1F3FD-200D-1F373","sheet_x":14,"sheet_y":2},"1F3FE":{"unified":"1F468-1F3FE-200D-1F373","sheet_x":14,"sheet_y":3},"1F3FF":{"unified":"1F468-1F3FF-200D-1F373","sheet_x":14,"sheet_y":4}}},{"unified":"1F468-200D-1F37C","sheet_x":14,"sheet_y":5,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F37C","sheet_x":14,"sheet_y":6},"1F3FC":{"unified":"1F468-1F3FC-200D-1F37C","sheet_x":14,"sheet_y":7},"1F3FD":{"unified":"1F468-1F3FD-200D-1F37C","sheet_x":14,"sheet_y":8},"1F3FE":{"unified":"1F468-1F3FE-200D-1F37C","sheet_x":14,"sheet_y":9},"1F3FF":{"unified":"1F468-1F3FF-200D-1F37C","sheet_x":14,"sheet_y":10}}},{"unified":"1F468-200D-1F393","sheet_x":14,"sheet_y":11,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F393","sheet_x":14,"sheet_y":12},"1F3FC":{"unified":"1F468-1F3FC-200D-1F393","sheet_x":14,"sheet_y":13},"1F3FD":{"unified":"1F468-1F3FD-200D-1F393","sheet_x":14,"sheet_y":14},"1F3FE":{"unified":"1F468-1F3FE-200D-1F393","sheet_x":14,"sheet_y":15},"1F3FF":{"unified":"1F468-1F3FF-200D-1F393","sheet_x":14,"sheet_y":16}}},{"unified":"1F468-200D-1F3A4","sheet_x":14,"sheet_y":17,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3A4","sheet_x":14,"sheet_y":18},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3A4","sheet_x":14,"sheet_y":19},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3A4","sheet_x":14,"sheet_y":20},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3A4","sheet_x":14,"sheet_y":21},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3A4","sheet_x":14,"sheet_y":22}}},{"unified":"1F468-200D-1F3A8","sheet_x":14,"sheet_y":23,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3A8","sheet_x":14,"sheet_y":24},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3A8","sheet_x":14,"sheet_y":25},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3A8","sheet_x":14,"sheet_y":26},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3A8","sheet_x":14,"sheet_y":27},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3A8","sheet_x":14,"sheet_y":28}}},{"unified":"1F468-200D-1F3EB","sheet_x":14,"sheet_y":29,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3EB","sheet_x":14,"sheet_y":30},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3EB","sheet_x":14,"sheet_y":31},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3EB","sheet_x":14,"sheet_y":32},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3EB","sheet_x":14,"sheet_y":33},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3EB","sheet_x":14,"sheet_y":34}}},{"unified":"1F468-200D-1F3ED","sheet_x":14,"sheet_y":35,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3ED","sheet_x":14,"sheet_y":36},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3ED","sheet_x":14,"sheet_y":37},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3ED","sheet_x":14,"sheet_y":38},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3ED","sheet_x":14,"sheet_y":39},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3ED","sheet_x":14,"sheet_y":40}}},{"unified":"1F468-200D-1F466-200D-1F466","sheet_x":14,"sheet_y":41,"skin_variations":{}},{"unified":"1F468-200D-1F466","sheet_x":14,"sheet_y":42,"skin_variations":{}},{"unified":"1F468-200D-1F467-200D-1F466","sheet_x":14,"sheet_y":43,"skin_variations":{}},{"unified":"1F468-200D-1F467-200D-1F467","sheet_x":14,"sheet_y":44,"skin_variations":{}},{"unified":"1F468-200D-1F467","sheet_x":14,"sheet_y":45,"skin_variations":{}},{"unified":"1F468-200D-1F468-200D-1F466","sheet_x":14,"sheet_y":46,"skin_variations":{}},{"unified":"1F468-200D-1F468-200D-1F466-200D-1F466","sheet_x":14,"sheet_y":47,"skin_variations":{}},{"unified":"1F468-200D-1F468-200D-1F467","sheet_x":14,"sheet_y":48,"skin_variations":{}},{"unified":"1F468-200D-1F468-200D-1F467-200D-1F466","sheet_x":14,"sheet_y":49,"skin_variations":{}},{"unified":"1F468-200D-1F468-200D-1F467-200D-1F467","sheet_x":14,"sheet_y":50,"skin_variations":{}},{"unified":"1F468-200D-1F469-200D-1F466","sheet_x":14,"sheet_y":51,"skin_variations":{}},{"unified":"1F468-200D-1F469-200D-1F466-200D-1F466","sheet_x":14,"sheet_y":52,"skin_variations":{}},{"unified":"1F468-200D-1F469-200D-1F467","sheet_x":14,"sheet_y":53,"skin_variations":{}},{"unified":"1F468-200D-1F469-200D-1F467-200D-1F466","sheet_x":14,"sheet_y":54,"skin_variations":{}},{"unified":"1F468-200D-1F469-200D-1F467-200D-1F467","sheet_x":14,"sheet_y":55,"skin_variations":{}},{"unified":"1F468-200D-1F4BB","sheet_x":14,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F4BB","sheet_x":14,"sheet_y":57},"1F3FC":{"unified":"1F468-1F3FC-200D-1F4BB","sheet_x":14,"sheet_y":58},"1F3FD":{"unified":"1F468-1F3FD-200D-1F4BB","sheet_x":14,"sheet_y":59},"1F3FE":{"unified":"1F468-1F3FE-200D-1F4BB","sheet_x":14,"sheet_y":60},"1F3FF":{"unified":"1F468-1F3FF-200D-1F4BB","sheet_x":14,"sheet_y":61}}},{"unified":"1F468-200D-1F4BC","sheet_x":15,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F4BC","sheet_x":15,"sheet_y":1},"1F3FC":{"unified":"1F468-1F3FC-200D-1F4BC","sheet_x":15,"sheet_y":2},"1F3FD":{"unified":"1F468-1F3FD-200D-1F4BC","sheet_x":15,"sheet_y":3},"1F3FE":{"unified":"1F468-1F3FE-200D-1F4BC","sheet_x":15,"sheet_y":4},"1F3FF":{"unified":"1F468-1F3FF-200D-1F4BC","sheet_x":15,"sheet_y":5}}},{"unified":"1F468-200D-1F527","sheet_x":15,"sheet_y":6,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F527","sheet_x":15,"sheet_y":7},"1F3FC":{"unified":"1F468-1F3FC-200D-1F527","sheet_x":15,"sheet_y":8},"1F3FD":{"unified":"1F468-1F3FD-200D-1F527","sheet_x":15,"sheet_y":9},"1F3FE":{"unified":"1F468-1F3FE-200D-1F527","sheet_x":15,"sheet_y":10},"1F3FF":{"unified":"1F468-1F3FF-200D-1F527","sheet_x":15,"sheet_y":11}}},{"unified":"1F468-200D-1F52C","sheet_x":15,"sheet_y":12,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F52C","sheet_x":15,"sheet_y":13},"1F3FC":{"unified":"1F468-1F3FC-200D-1F52C","sheet_x":15,"sheet_y":14},"1F3FD":{"unified":"1F468-1F3FD-200D-1F52C","sheet_x":15,"sheet_y":15},"1F3FE":{"unified":"1F468-1F3FE-200D-1F52C","sheet_x":15,"sheet_y":16},"1F3FF":{"unified":"1F468-1F3FF-200D-1F52C","sheet_x":15,"sheet_y":17}}},{"unified":"1F468-200D-1F680","sheet_x":15,"sheet_y":18,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F680","sheet_x":15,"sheet_y":19},"1F3FC":{"unified":"1F468-1F3FC-200D-1F680","sheet_x":15,"sheet_y":20},"1F3FD":{"unified":"1F468-1F3FD-200D-1F680","sheet_x":15,"sheet_y":21},"1F3FE":{"unified":"1F468-1F3FE-200D-1F680","sheet_x":15,"sheet_y":22},"1F3FF":{"unified":"1F468-1F3FF-200D-1F680","sheet_x":15,"sheet_y":23}}},{"unified":"1F468-200D-1F692","sheet_x":15,"sheet_y":24,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F692","sheet_x":15,"sheet_y":25},"1F3FC":{"unified":"1F468-1F3FC-200D-1F692","sheet_x":15,"sheet_y":26},"1F3FD":{"unified":"1F468-1F3FD-200D-1F692","sheet_x":15,"sheet_y":27},"1F3FE":{"unified":"1F468-1F3FE-200D-1F692","sheet_x":15,"sheet_y":28},"1F3FF":{"unified":"1F468-1F3FF-200D-1F692","sheet_x":15,"sheet_y":29}}},{"unified":"1F468-200D-1F9AF-200D-27A1-FE0F","sheet_x":15,"sheet_y":30,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9AF-200D-27A1-FE0F","sheet_x":15,"sheet_y":31},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9AF-200D-27A1-FE0F","sheet_x":15,"sheet_y":32},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9AF-200D-27A1-FE0F","sheet_x":15,"sheet_y":33},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9AF-200D-27A1-FE0F","sheet_x":15,"sheet_y":34},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9AF-200D-27A1-FE0F","sheet_x":15,"sheet_y":35}}},{"unified":"1F468-200D-1F9AF","sheet_x":15,"sheet_y":36,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9AF","sheet_x":15,"sheet_y":37},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9AF","sheet_x":15,"sheet_y":38},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9AF","sheet_x":15,"sheet_y":39},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9AF","sheet_x":15,"sheet_y":40},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9AF","sheet_x":15,"sheet_y":41}}},{"unified":"1F468-200D-1F9B0","sheet_x":15,"sheet_y":42,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B0","sheet_x":15,"sheet_y":43},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B0","sheet_x":15,"sheet_y":44},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B0","sheet_x":15,"sheet_y":45},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B0","sheet_x":15,"sheet_y":46},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B0","sheet_x":15,"sheet_y":47}}},{"unified":"1F468-200D-1F9B1","sheet_x":15,"sheet_y":48,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B1","sheet_x":15,"sheet_y":49},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B1","sheet_x":15,"sheet_y":50},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B1","sheet_x":15,"sheet_y":51},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B1","sheet_x":15,"sheet_y":52},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B1","sheet_x":15,"sheet_y":53}}},{"unified":"1F468-200D-1F9B2","sheet_x":15,"sheet_y":54,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B2","sheet_x":15,"sheet_y":55},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B2","sheet_x":15,"sheet_y":56},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B2","sheet_x":15,"sheet_y":57},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B2","sheet_x":15,"sheet_y":58},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B2","sheet_x":15,"sheet_y":59}}},{"unified":"1F468-200D-1F9B3","sheet_x":15,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B3","sheet_x":15,"sheet_y":61},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B3","sheet_x":16,"sheet_y":0},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B3","sheet_x":16,"sheet_y":1},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B3","sheet_x":16,"sheet_y":2},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B3","sheet_x":16,"sheet_y":3}}},{"unified":"1F468-200D-1F9BC-200D-27A1-FE0F","sheet_x":16,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BC-200D-27A1-FE0F","sheet_x":16,"sheet_y":5},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BC-200D-27A1-FE0F","sheet_x":16,"sheet_y":6},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BC-200D-27A1-FE0F","sheet_x":16,"sheet_y":7},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BC-200D-27A1-FE0F","sheet_x":16,"sheet_y":8},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BC-200D-27A1-FE0F","sheet_x":16,"sheet_y":9}}},{"unified":"1F468-200D-1F9BC","sheet_x":16,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BC","sheet_x":16,"sheet_y":11},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BC","sheet_x":16,"sheet_y":12},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BC","sheet_x":16,"sheet_y":13},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BC","sheet_x":16,"sheet_y":14},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BC","sheet_x":16,"sheet_y":15}}},{"unified":"1F468-200D-1F9BD-200D-27A1-FE0F","sheet_x":16,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BD-200D-27A1-FE0F","sheet_x":16,"sheet_y":17},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BD-200D-27A1-FE0F","sheet_x":16,"sheet_y":18},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BD-200D-27A1-FE0F","sheet_x":16,"sheet_y":19},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BD-200D-27A1-FE0F","sheet_x":16,"sheet_y":20},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BD-200D-27A1-FE0F","sheet_x":16,"sheet_y":21}}},{"unified":"1F468-200D-1F9BD","sheet_x":16,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BD","sheet_x":16,"sheet_y":23},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BD","sheet_x":16,"sheet_y":24},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BD","sheet_x":16,"sheet_y":25},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BD","sheet_x":16,"sheet_y":26},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BD","sheet_x":16,"sheet_y":27}}},{"unified":"1F468-200D-2695-FE0F","sheet_x":16,"sheet_y":28,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2695-FE0F","sheet_x":16,"sheet_y":29},"1F3FC":{"unified":"1F468-1F3FC-200D-2695-FE0F","sheet_x":16,"sheet_y":30},"1F3FD":{"unified":"1F468-1F3FD-200D-2695-FE0F","sheet_x":16,"sheet_y":31},"1F3FE":{"unified":"1F468-1F3FE-200D-2695-FE0F","sheet_x":16,"sheet_y":32},"1F3FF":{"unified":"1F468-1F3FF-200D-2695-FE0F","sheet_x":16,"sheet_y":33}}},{"unified":"1F468-200D-2696-FE0F","sheet_x":16,"sheet_y":34,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2696-FE0F","sheet_x":16,"sheet_y":35},"1F3FC":{"unified":"1F468-1F3FC-200D-2696-FE0F","sheet_x":16,"sheet_y":36},"1F3FD":{"unified":"1F468-1F3FD-200D-2696-FE0F","sheet_x":16,"sheet_y":37},"1F3FE":{"unified":"1F468-1F3FE-200D-2696-FE0F","sheet_x":16,"sheet_y":38},"1F3FF":{"unified":"1F468-1F3FF-200D-2696-FE0F","sheet_x":16,"sheet_y":39}}},{"unified":"1F468-200D-2708-FE0F","sheet_x":16,"sheet_y":40,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2708-FE0F","sheet_x":16,"sheet_y":41},"1F3FC":{"unified":"1F468-1F3FC-200D-2708-FE0F","sheet_x":16,"sheet_y":42},"1F3FD":{"unified":"1F468-1F3FD-200D-2708-FE0F","sheet_x":16,"sheet_y":43},"1F3FE":{"unified":"1F468-1F3FE-200D-2708-FE0F","sheet_x":16,"sheet_y":44},"1F3FF":{"unified":"1F468-1F3FF-200D-2708-FE0F","sheet_x":16,"sheet_y":45}}},{"unified":"1F468-200D-2764-FE0F-200D-1F468","sheet_x":16,"sheet_y":46,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":16,"sheet_y":47},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":16,"sheet_y":48},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":16,"sheet_y":49},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":16,"sheet_y":50},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":16,"sheet_y":51},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":16,"sheet_y":52},"1F3FC-1F3FC":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":16,"sheet_y":53},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":16,"sheet_y":54},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":16,"sheet_y":55},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":16,"sheet_y":56},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":16,"sheet_y":57},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":16,"sheet_y":58},"1F3FD-1F3FD":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":16,"sheet_y":59},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":16,"sheet_y":60},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":16,"sheet_y":61},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":17,"sheet_y":0},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":17,"sheet_y":1},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":17,"sheet_y":2},"1F3FE-1F3FE":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":17,"sheet_y":3},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":17,"sheet_y":4},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":17,"sheet_y":5},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":17,"sheet_y":6},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":17,"sheet_y":7},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":17,"sheet_y":8},"1F3FF-1F3FF":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":17,"sheet_y":9}}},{"unified":"1F468-200D-2764-FE0F-200D-1F48B-200D-1F468","sheet_x":17,"sheet_y":10,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":17,"sheet_y":11},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":17,"sheet_y":12},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":17,"sheet_y":13},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":17,"sheet_y":14},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":17,"sheet_y":15},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":17,"sheet_y":16},"1F3FC-1F3FC":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":17,"sheet_y":17},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":17,"sheet_y":18},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":17,"sheet_y":19},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":17,"sheet_y":20},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":17,"sheet_y":21},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":17,"sheet_y":22},"1F3FD-1F3FD":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":17,"sheet_y":23},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":17,"sheet_y":24},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":17,"sheet_y":25},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":17,"sheet_y":26},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":17,"sheet_y":27},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":17,"sheet_y":28},"1F3FE-1F3FE":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":17,"sheet_y":29},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":17,"sheet_y":30},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":17,"sheet_y":31},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":17,"sheet_y":32},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":17,"sheet_y":33},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":17,"sheet_y":34},"1F3FF-1F3FF":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":17,"sheet_y":35}}},{"unified":"1F468","sheet_x":17,"sheet_y":36,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB","sheet_x":17,"sheet_y":37},"1F3FC":{"unified":"1F468-1F3FC","sheet_x":17,"sheet_y":38},"1F3FD":{"unified":"1F468-1F3FD","sheet_x":17,"sheet_y":39},"1F3FE":{"unified":"1F468-1F3FE","sheet_x":17,"sheet_y":40},"1F3FF":{"unified":"1F468-1F3FF","sheet_x":17,"sheet_y":41}}},{"unified":"1F469-200D-1F33E","sheet_x":17,"sheet_y":42,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F33E","sheet_x":17,"sheet_y":43},"1F3FC":{"unified":"1F469-1F3FC-200D-1F33E","sheet_x":17,"sheet_y":44},"1F3FD":{"unified":"1F469-1F3FD-200D-1F33E","sheet_x":17,"sheet_y":45},"1F3FE":{"unified":"1F469-1F3FE-200D-1F33E","sheet_x":17,"sheet_y":46},"1F3FF":{"unified":"1F469-1F3FF-200D-1F33E","sheet_x":17,"sheet_y":47}}},{"unified":"1F469-200D-1F373","sheet_x":17,"sheet_y":48,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F373","sheet_x":17,"sheet_y":49},"1F3FC":{"unified":"1F469-1F3FC-200D-1F373","sheet_x":17,"sheet_y":50},"1F3FD":{"unified":"1F469-1F3FD-200D-1F373","sheet_x":17,"sheet_y":51},"1F3FE":{"unified":"1F469-1F3FE-200D-1F373","sheet_x":17,"sheet_y":52},"1F3FF":{"unified":"1F469-1F3FF-200D-1F373","sheet_x":17,"sheet_y":53}}},{"unified":"1F469-200D-1F37C","sheet_x":17,"sheet_y":54,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F37C","sheet_x":17,"sheet_y":55},"1F3FC":{"unified":"1F469-1F3FC-200D-1F37C","sheet_x":17,"sheet_y":56},"1F3FD":{"unified":"1F469-1F3FD-200D-1F37C","sheet_x":17,"sheet_y":57},"1F3FE":{"unified":"1F469-1F3FE-200D-1F37C","sheet_x":17,"sheet_y":58},"1F3FF":{"unified":"1F469-1F3FF-200D-1F37C","sheet_x":17,"sheet_y":59}}},{"unified":"1F469-200D-1F393","sheet_x":17,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F393","sheet_x":17,"sheet_y":61},"1F3FC":{"unified":"1F469-1F3FC-200D-1F393","sheet_x":18,"sheet_y":0},"1F3FD":{"unified":"1F469-1F3FD-200D-1F393","sheet_x":18,"sheet_y":1},"1F3FE":{"unified":"1F469-1F3FE-200D-1F393","sheet_x":18,"sheet_y":2},"1F3FF":{"unified":"1F469-1F3FF-200D-1F393","sheet_x":18,"sheet_y":3}}},{"unified":"1F469-200D-1F3A4","sheet_x":18,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3A4","sheet_x":18,"sheet_y":5},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3A4","sheet_x":18,"sheet_y":6},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3A4","sheet_x":18,"sheet_y":7},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3A4","sheet_x":18,"sheet_y":8},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3A4","sheet_x":18,"sheet_y":9}}},{"unified":"1F469-200D-1F3A8","sheet_x":18,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3A8","sheet_x":18,"sheet_y":11},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3A8","sheet_x":18,"sheet_y":12},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3A8","sheet_x":18,"sheet_y":13},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3A8","sheet_x":18,"sheet_y":14},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3A8","sheet_x":18,"sheet_y":15}}},{"unified":"1F469-200D-1F3EB","sheet_x":18,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3EB","sheet_x":18,"sheet_y":17},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3EB","sheet_x":18,"sheet_y":18},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3EB","sheet_x":18,"sheet_y":19},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3EB","sheet_x":18,"sheet_y":20},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3EB","sheet_x":18,"sheet_y":21}}},{"unified":"1F469-200D-1F3ED","sheet_x":18,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3ED","sheet_x":18,"sheet_y":23},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3ED","sheet_x":18,"sheet_y":24},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3ED","sheet_x":18,"sheet_y":25},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3ED","sheet_x":18,"sheet_y":26},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3ED","sheet_x":18,"sheet_y":27}}},{"unified":"1F469-200D-1F466-200D-1F466","sheet_x":18,"sheet_y":28,"skin_variations":{}},{"unified":"1F469-200D-1F466","sheet_x":18,"sheet_y":29,"skin_variations":{}},{"unified":"1F469-200D-1F467-200D-1F466","sheet_x":18,"sheet_y":30,"skin_variations":{}},{"unified":"1F469-200D-1F467-200D-1F467","sheet_x":18,"sheet_y":31,"skin_variations":{}},{"unified":"1F469-200D-1F467","sheet_x":18,"sheet_y":32,"skin_variations":{}},{"unified":"1F469-200D-1F469-200D-1F466","sheet_x":18,"sheet_y":33,"skin_variations":{}},{"unified":"1F469-200D-1F469-200D-1F466-200D-1F466","sheet_x":18,"sheet_y":34,"skin_variations":{}},{"unified":"1F469-200D-1F469-200D-1F467","sheet_x":18,"sheet_y":35,"skin_variations":{}},{"unified":"1F469-200D-1F469-200D-1F467-200D-1F466","sheet_x":18,"sheet_y":36,"skin_variations":{}},{"unified":"1F469-200D-1F469-200D-1F467-200D-1F467","sheet_x":18,"sheet_y":37,"skin_variations":{}},{"unified":"1F469-200D-1F4BB","sheet_x":18,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F4BB","sheet_x":18,"sheet_y":39},"1F3FC":{"unified":"1F469-1F3FC-200D-1F4BB","sheet_x":18,"sheet_y":40},"1F3FD":{"unified":"1F469-1F3FD-200D-1F4BB","sheet_x":18,"sheet_y":41},"1F3FE":{"unified":"1F469-1F3FE-200D-1F4BB","sheet_x":18,"sheet_y":42},"1F3FF":{"unified":"1F469-1F3FF-200D-1F4BB","sheet_x":18,"sheet_y":43}}},{"unified":"1F469-200D-1F4BC","sheet_x":18,"sheet_y":44,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F4BC","sheet_x":18,"sheet_y":45},"1F3FC":{"unified":"1F469-1F3FC-200D-1F4BC","sheet_x":18,"sheet_y":46},"1F3FD":{"unified":"1F469-1F3FD-200D-1F4BC","sheet_x":18,"sheet_y":47},"1F3FE":{"unified":"1F469-1F3FE-200D-1F4BC","sheet_x":18,"sheet_y":48},"1F3FF":{"unified":"1F469-1F3FF-200D-1F4BC","sheet_x":18,"sheet_y":49}}},{"unified":"1F469-200D-1F527","sheet_x":18,"sheet_y":50,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F527","sheet_x":18,"sheet_y":51},"1F3FC":{"unified":"1F469-1F3FC-200D-1F527","sheet_x":18,"sheet_y":52},"1F3FD":{"unified":"1F469-1F3FD-200D-1F527","sheet_x":18,"sheet_y":53},"1F3FE":{"unified":"1F469-1F3FE-200D-1F527","sheet_x":18,"sheet_y":54},"1F3FF":{"unified":"1F469-1F3FF-200D-1F527","sheet_x":18,"sheet_y":55}}},{"unified":"1F469-200D-1F52C","sheet_x":18,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F52C","sheet_x":18,"sheet_y":57},"1F3FC":{"unified":"1F469-1F3FC-200D-1F52C","sheet_x":18,"sheet_y":58},"1F3FD":{"unified":"1F469-1F3FD-200D-1F52C","sheet_x":18,"sheet_y":59},"1F3FE":{"unified":"1F469-1F3FE-200D-1F52C","sheet_x":18,"sheet_y":60},"1F3FF":{"unified":"1F469-1F3FF-200D-1F52C","sheet_x":18,"sheet_y":61}}},{"unified":"1F469-200D-1F680","sheet_x":19,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F680","sheet_x":19,"sheet_y":1},"1F3FC":{"unified":"1F469-1F3FC-200D-1F680","sheet_x":19,"sheet_y":2},"1F3FD":{"unified":"1F469-1F3FD-200D-1F680","sheet_x":19,"sheet_y":3},"1F3FE":{"unified":"1F469-1F3FE-200D-1F680","sheet_x":19,"sheet_y":4},"1F3FF":{"unified":"1F469-1F3FF-200D-1F680","sheet_x":19,"sheet_y":5}}},{"unified":"1F469-200D-1F692","sheet_x":19,"sheet_y":6,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F692","sheet_x":19,"sheet_y":7},"1F3FC":{"unified":"1F469-1F3FC-200D-1F692","sheet_x":19,"sheet_y":8},"1F3FD":{"unified":"1F469-1F3FD-200D-1F692","sheet_x":19,"sheet_y":9},"1F3FE":{"unified":"1F469-1F3FE-200D-1F692","sheet_x":19,"sheet_y":10},"1F3FF":{"unified":"1F469-1F3FF-200D-1F692","sheet_x":19,"sheet_y":11}}},{"unified":"1F469-200D-1F9AF-200D-27A1-FE0F","sheet_x":19,"sheet_y":12,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9AF-200D-27A1-FE0F","sheet_x":19,"sheet_y":13},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9AF-200D-27A1-FE0F","sheet_x":19,"sheet_y":14},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9AF-200D-27A1-FE0F","sheet_x":19,"sheet_y":15},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9AF-200D-27A1-FE0F","sheet_x":19,"sheet_y":16},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9AF-200D-27A1-FE0F","sheet_x":19,"sheet_y":17}}},{"unified":"1F469-200D-1F9AF","sheet_x":19,"sheet_y":18,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9AF","sheet_x":19,"sheet_y":19},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9AF","sheet_x":19,"sheet_y":20},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9AF","sheet_x":19,"sheet_y":21},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9AF","sheet_x":19,"sheet_y":22},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9AF","sheet_x":19,"sheet_y":23}}},{"unified":"1F469-200D-1F9B0","sheet_x":19,"sheet_y":24,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B0","sheet_x":19,"sheet_y":25},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B0","sheet_x":19,"sheet_y":26},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B0","sheet_x":19,"sheet_y":27},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B0","sheet_x":19,"sheet_y":28},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B0","sheet_x":19,"sheet_y":29}}},{"unified":"1F469-200D-1F9B1","sheet_x":19,"sheet_y":30,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B1","sheet_x":19,"sheet_y":31},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B1","sheet_x":19,"sheet_y":32},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B1","sheet_x":19,"sheet_y":33},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B1","sheet_x":19,"sheet_y":34},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B1","sheet_x":19,"sheet_y":35}}},{"unified":"1F469-200D-1F9B2","sheet_x":19,"sheet_y":36,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B2","sheet_x":19,"sheet_y":37},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B2","sheet_x":19,"sheet_y":38},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B2","sheet_x":19,"sheet_y":39},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B2","sheet_x":19,"sheet_y":40},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B2","sheet_x":19,"sheet_y":41}}},{"unified":"1F469-200D-1F9B3","sheet_x":19,"sheet_y":42,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B3","sheet_x":19,"sheet_y":43},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B3","sheet_x":19,"sheet_y":44},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B3","sheet_x":19,"sheet_y":45},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B3","sheet_x":19,"sheet_y":46},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B3","sheet_x":19,"sheet_y":47}}},{"unified":"1F469-200D-1F9BC-200D-27A1-FE0F","sheet_x":19,"sheet_y":48,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BC-200D-27A1-FE0F","sheet_x":19,"sheet_y":49},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BC-200D-27A1-FE0F","sheet_x":19,"sheet_y":50},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BC-200D-27A1-FE0F","sheet_x":19,"sheet_y":51},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BC-200D-27A1-FE0F","sheet_x":19,"sheet_y":52},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BC-200D-27A1-FE0F","sheet_x":19,"sheet_y":53}}},{"unified":"1F469-200D-1F9BC","sheet_x":19,"sheet_y":54,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BC","sheet_x":19,"sheet_y":55},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BC","sheet_x":19,"sheet_y":56},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BC","sheet_x":19,"sheet_y":57},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BC","sheet_x":19,"sheet_y":58},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BC","sheet_x":19,"sheet_y":59}}},{"unified":"1F469-200D-1F9BD-200D-27A1-FE0F","sheet_x":19,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BD-200D-27A1-FE0F","sheet_x":19,"sheet_y":61},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BD-200D-27A1-FE0F","sheet_x":20,"sheet_y":0},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BD-200D-27A1-FE0F","sheet_x":20,"sheet_y":1},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BD-200D-27A1-FE0F","sheet_x":20,"sheet_y":2},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BD-200D-27A1-FE0F","sheet_x":20,"sheet_y":3}}},{"unified":"1F469-200D-1F9BD","sheet_x":20,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BD","sheet_x":20,"sheet_y":5},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BD","sheet_x":20,"sheet_y":6},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BD","sheet_x":20,"sheet_y":7},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BD","sheet_x":20,"sheet_y":8},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BD","sheet_x":20,"sheet_y":9}}},{"unified":"1F469-200D-2695-FE0F","sheet_x":20,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2695-FE0F","sheet_x":20,"sheet_y":11},"1F3FC":{"unified":"1F469-1F3FC-200D-2695-FE0F","sheet_x":20,"sheet_y":12},"1F3FD":{"unified":"1F469-1F3FD-200D-2695-FE0F","sheet_x":20,"sheet_y":13},"1F3FE":{"unified":"1F469-1F3FE-200D-2695-FE0F","sheet_x":20,"sheet_y":14},"1F3FF":{"unified":"1F469-1F3FF-200D-2695-FE0F","sheet_x":20,"sheet_y":15}}},{"unified":"1F469-200D-2696-FE0F","sheet_x":20,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2696-FE0F","sheet_x":20,"sheet_y":17},"1F3FC":{"unified":"1F469-1F3FC-200D-2696-FE0F","sheet_x":20,"sheet_y":18},"1F3FD":{"unified":"1F469-1F3FD-200D-2696-FE0F","sheet_x":20,"sheet_y":19},"1F3FE":{"unified":"1F469-1F3FE-200D-2696-FE0F","sheet_x":20,"sheet_y":20},"1F3FF":{"unified":"1F469-1F3FF-200D-2696-FE0F","sheet_x":20,"sheet_y":21}}},{"unified":"1F469-200D-2708-FE0F","sheet_x":20,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2708-FE0F","sheet_x":20,"sheet_y":23},"1F3FC":{"unified":"1F469-1F3FC-200D-2708-FE0F","sheet_x":20,"sheet_y":24},"1F3FD":{"unified":"1F469-1F3FD-200D-2708-FE0F","sheet_x":20,"sheet_y":25},"1F3FE":{"unified":"1F469-1F3FE-200D-2708-FE0F","sheet_x":20,"sheet_y":26},"1F3FF":{"unified":"1F469-1F3FF-200D-2708-FE0F","sheet_x":20,"sheet_y":27}}},{"unified":"1F469-200D-2764-FE0F-200D-1F468","sheet_x":20,"sheet_y":28,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":20,"sheet_y":29},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":20,"sheet_y":30},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":20,"sheet_y":31},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":20,"sheet_y":32},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":20,"sheet_y":33},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":20,"sheet_y":34},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":20,"sheet_y":35},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":20,"sheet_y":36},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":20,"sheet_y":37},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":20,"sheet_y":38},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":20,"sheet_y":39},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":20,"sheet_y":40},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":20,"sheet_y":41},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":20,"sheet_y":42},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":20,"sheet_y":43},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":20,"sheet_y":44},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":20,"sheet_y":45},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":20,"sheet_y":46},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":20,"sheet_y":47},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":20,"sheet_y":48},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FB","sheet_x":20,"sheet_y":49},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FC","sheet_x":20,"sheet_y":50},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FD","sheet_x":20,"sheet_y":51},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FE","sheet_x":20,"sheet_y":52},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FF","sheet_x":20,"sheet_y":53}}},{"unified":"1F469-200D-2764-FE0F-200D-1F469","sheet_x":20,"sheet_y":54,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FB","sheet_x":20,"sheet_y":55},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FC","sheet_x":20,"sheet_y":56},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FD","sheet_x":20,"sheet_y":57},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FE","sheet_x":20,"sheet_y":58},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FF","sheet_x":20,"sheet_y":59},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FB","sheet_x":20,"sheet_y":60},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FC","sheet_x":20,"sheet_y":61},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FD","sheet_x":21,"sheet_y":0},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FE","sheet_x":21,"sheet_y":1},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FF","sheet_x":21,"sheet_y":2},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FB","sheet_x":21,"sheet_y":3},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FC","sheet_x":21,"sheet_y":4},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FD","sheet_x":21,"sheet_y":5},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FE","sheet_x":21,"sheet_y":6},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FF","sheet_x":21,"sheet_y":7},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FB","sheet_x":21,"sheet_y":8},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FC","sheet_x":21,"sheet_y":9},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FD","sheet_x":21,"sheet_y":10},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FE","sheet_x":21,"sheet_y":11},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FF","sheet_x":21,"sheet_y":12},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FB","sheet_x":21,"sheet_y":13},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FC","sheet_x":21,"sheet_y":14},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FD","sheet_x":21,"sheet_y":15},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FE","sheet_x":21,"sheet_y":16},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FF","sheet_x":21,"sheet_y":17}}},{"unified":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F468","sheet_x":21,"sheet_y":18,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":21,"sheet_y":19},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":21,"sheet_y":20},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":21,"sheet_y":21},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":21,"sheet_y":22},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":21,"sheet_y":23},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":21,"sheet_y":24},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":21,"sheet_y":25},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":21,"sheet_y":26},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":21,"sheet_y":27},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":21,"sheet_y":28},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":21,"sheet_y":29},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":21,"sheet_y":30},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":21,"sheet_y":31},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":21,"sheet_y":32},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":21,"sheet_y":33},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":21,"sheet_y":34},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":21,"sheet_y":35},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":21,"sheet_y":36},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":21,"sheet_y":37},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":21,"sheet_y":38},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","sheet_x":21,"sheet_y":39},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","sheet_x":21,"sheet_y":40},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","sheet_x":21,"sheet_y":41},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","sheet_x":21,"sheet_y":42},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","sheet_x":21,"sheet_y":43}}},{"unified":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F469","sheet_x":21,"sheet_y":44,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","sheet_x":21,"sheet_y":45},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","sheet_x":21,"sheet_y":46},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","sheet_x":21,"sheet_y":47},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","sheet_x":21,"sheet_y":48},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","sheet_x":21,"sheet_y":49},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","sheet_x":21,"sheet_y":50},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","sheet_x":21,"sheet_y":51},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","sheet_x":21,"sheet_y":52},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","sheet_x":21,"sheet_y":53},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","sheet_x":21,"sheet_y":54},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","sheet_x":21,"sheet_y":55},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","sheet_x":21,"sheet_y":56},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","sheet_x":21,"sheet_y":57},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","sheet_x":21,"sheet_y":58},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","sheet_x":21,"sheet_y":59},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","sheet_x":21,"sheet_y":60},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","sheet_x":21,"sheet_y":61},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","sheet_x":22,"sheet_y":0},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","sheet_x":22,"sheet_y":1},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","sheet_x":22,"sheet_y":2},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","sheet_x":22,"sheet_y":3},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","sheet_x":22,"sheet_y":4},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","sheet_x":22,"sheet_y":5},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","sheet_x":22,"sheet_y":6},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","sheet_x":22,"sheet_y":7}}},{"unified":"1F469","sheet_x":22,"sheet_y":8,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB","sheet_x":22,"sheet_y":9},"1F3FC":{"unified":"1F469-1F3FC","sheet_x":22,"sheet_y":10},"1F3FD":{"unified":"1F469-1F3FD","sheet_x":22,"sheet_y":11},"1F3FE":{"unified":"1F469-1F3FE","sheet_x":22,"sheet_y":12},"1F3FF":{"unified":"1F469-1F3FF","sheet_x":22,"sheet_y":13}}},{"unified":"1F46A","sheet_x":22,"sheet_y":14,"skin_variations":{}},{"unified":"1F46B","sheet_x":22,"sheet_y":15,"skin_variations":{"1F3FB":{"unified":"1F46B-1F3FB","sheet_x":22,"sheet_y":16},"1F3FC":{"unified":"1F46B-1F3FC","sheet_x":22,"sheet_y":17},"1F3FD":{"unified":"1F46B-1F3FD","sheet_x":22,"sheet_y":18},"1F3FE":{"unified":"1F46B-1F3FE","sheet_x":22,"sheet_y":19},"1F3FF":{"unified":"1F46B-1F3FF","sheet_x":22,"sheet_y":20},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FC","sheet_x":22,"sheet_y":21},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FD","sheet_x":22,"sheet_y":22},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FE","sheet_x":22,"sheet_y":23},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FF","sheet_x":22,"sheet_y":24},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FB","sheet_x":22,"sheet_y":25},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FD","sheet_x":22,"sheet_y":26},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FE","sheet_x":22,"sheet_y":27},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FF","sheet_x":22,"sheet_y":28},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FB","sheet_x":22,"sheet_y":29},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FC","sheet_x":22,"sheet_y":30},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FE","sheet_x":22,"sheet_y":31},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FF","sheet_x":22,"sheet_y":32},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FB","sheet_x":22,"sheet_y":33},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FC","sheet_x":22,"sheet_y":34},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FD","sheet_x":22,"sheet_y":35},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FF","sheet_x":22,"sheet_y":36},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FB","sheet_x":22,"sheet_y":37},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FC","sheet_x":22,"sheet_y":38},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FD","sheet_x":22,"sheet_y":39},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FE","sheet_x":22,"sheet_y":40}}},{"unified":"1F46C","sheet_x":22,"sheet_y":41,"skin_variations":{"1F3FB":{"unified":"1F46C-1F3FB","sheet_x":22,"sheet_y":42},"1F3FC":{"unified":"1F46C-1F3FC","sheet_x":22,"sheet_y":43},"1F3FD":{"unified":"1F46C-1F3FD","sheet_x":22,"sheet_y":44},"1F3FE":{"unified":"1F46C-1F3FE","sheet_x":22,"sheet_y":45},"1F3FF":{"unified":"1F46C-1F3FF","sheet_x":22,"sheet_y":46},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FC","sheet_x":22,"sheet_y":47},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FD","sheet_x":22,"sheet_y":48},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FE","sheet_x":22,"sheet_y":49},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FF","sheet_x":22,"sheet_y":50},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FB","sheet_x":22,"sheet_y":51},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FD","sheet_x":22,"sheet_y":52},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FE","sheet_x":22,"sheet_y":53},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FF","sheet_x":22,"sheet_y":54},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FB","sheet_x":22,"sheet_y":55},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FC","sheet_x":22,"sheet_y":56},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FE","sheet_x":22,"sheet_y":57},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FF","sheet_x":22,"sheet_y":58},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FB","sheet_x":22,"sheet_y":59},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FC","sheet_x":22,"sheet_y":60},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FD","sheet_x":22,"sheet_y":61},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FF","sheet_x":23,"sheet_y":0},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FB","sheet_x":23,"sheet_y":1},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FC","sheet_x":23,"sheet_y":2},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FD","sheet_x":23,"sheet_y":3},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FE","sheet_x":23,"sheet_y":4}}},{"unified":"1F46D","sheet_x":23,"sheet_y":5,"skin_variations":{"1F3FB":{"unified":"1F46D-1F3FB","sheet_x":23,"sheet_y":6},"1F3FC":{"unified":"1F46D-1F3FC","sheet_x":23,"sheet_y":7},"1F3FD":{"unified":"1F46D-1F3FD","sheet_x":23,"sheet_y":8},"1F3FE":{"unified":"1F46D-1F3FE","sheet_x":23,"sheet_y":9},"1F3FF":{"unified":"1F46D-1F3FF","sheet_x":23,"sheet_y":10},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FC","sheet_x":23,"sheet_y":11},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FD","sheet_x":23,"sheet_y":12},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FE","sheet_x":23,"sheet_y":13},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FF","sheet_x":23,"sheet_y":14},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FB","sheet_x":23,"sheet_y":15},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FD","sheet_x":23,"sheet_y":16},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FE","sheet_x":23,"sheet_y":17},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FF","sheet_x":23,"sheet_y":18},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FB","sheet_x":23,"sheet_y":19},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FC","sheet_x":23,"sheet_y":20},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FE","sheet_x":23,"sheet_y":21},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FF","sheet_x":23,"sheet_y":22},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FB","sheet_x":23,"sheet_y":23},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FC","sheet_x":23,"sheet_y":24},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FD","sheet_x":23,"sheet_y":25},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FF","sheet_x":23,"sheet_y":26},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FB","sheet_x":23,"sheet_y":27},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FC","sheet_x":23,"sheet_y":28},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FD","sheet_x":23,"sheet_y":29},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FE","sheet_x":23,"sheet_y":30}}},{"unified":"1F46E-200D-2640-FE0F","sheet_x":23,"sheet_y":31,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB-200D-2640-FE0F","sheet_x":23,"sheet_y":32},"1F3FC":{"unified":"1F46E-1F3FC-200D-2640-FE0F","sheet_x":23,"sheet_y":33},"1F3FD":{"unified":"1F46E-1F3FD-200D-2640-FE0F","sheet_x":23,"sheet_y":34},"1F3FE":{"unified":"1F46E-1F3FE-200D-2640-FE0F","sheet_x":23,"sheet_y":35},"1F3FF":{"unified":"1F46E-1F3FF-200D-2640-FE0F","sheet_x":23,"sheet_y":36}}},{"unified":"1F46E-200D-2642-FE0F","sheet_x":23,"sheet_y":37,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB-200D-2642-FE0F","sheet_x":23,"sheet_y":38},"1F3FC":{"unified":"1F46E-1F3FC-200D-2642-FE0F","sheet_x":23,"sheet_y":39},"1F3FD":{"unified":"1F46E-1F3FD-200D-2642-FE0F","sheet_x":23,"sheet_y":40},"1F3FE":{"unified":"1F46E-1F3FE-200D-2642-FE0F","sheet_x":23,"sheet_y":41},"1F3FF":{"unified":"1F46E-1F3FF-200D-2642-FE0F","sheet_x":23,"sheet_y":42}}},{"unified":"1F46E","sheet_x":23,"sheet_y":43,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB","sheet_x":23,"sheet_y":44},"1F3FC":{"unified":"1F46E-1F3FC","sheet_x":23,"sheet_y":45},"1F3FD":{"unified":"1F46E-1F3FD","sheet_x":23,"sheet_y":46},"1F3FE":{"unified":"1F46E-1F3FE","sheet_x":23,"sheet_y":47},"1F3FF":{"unified":"1F46E-1F3FF","sheet_x":23,"sheet_y":48}}},{"unified":"1F46F-200D-2640-FE0F","sheet_x":23,"sheet_y":49,"skin_variations":{}},{"unified":"1F46F-200D-2642-FE0F","sheet_x":23,"sheet_y":50,"skin_variations":{}},{"unified":"1F46F","sheet_x":23,"sheet_y":51,"skin_variations":{}},{"unified":"1F470-200D-2640-FE0F","sheet_x":23,"sheet_y":52,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB-200D-2640-FE0F","sheet_x":23,"sheet_y":53},"1F3FC":{"unified":"1F470-1F3FC-200D-2640-FE0F","sheet_x":23,"sheet_y":54},"1F3FD":{"unified":"1F470-1F3FD-200D-2640-FE0F","sheet_x":23,"sheet_y":55},"1F3FE":{"unified":"1F470-1F3FE-200D-2640-FE0F","sheet_x":23,"sheet_y":56},"1F3FF":{"unified":"1F470-1F3FF-200D-2640-FE0F","sheet_x":23,"sheet_y":57}}},{"unified":"1F470-200D-2642-FE0F","sheet_x":23,"sheet_y":58,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB-200D-2642-FE0F","sheet_x":23,"sheet_y":59},"1F3FC":{"unified":"1F470-1F3FC-200D-2642-FE0F","sheet_x":23,"sheet_y":60},"1F3FD":{"unified":"1F470-1F3FD-200D-2642-FE0F","sheet_x":23,"sheet_y":61},"1F3FE":{"unified":"1F470-1F3FE-200D-2642-FE0F","sheet_x":24,"sheet_y":0},"1F3FF":{"unified":"1F470-1F3FF-200D-2642-FE0F","sheet_x":24,"sheet_y":1}}},{"unified":"1F470","sheet_x":24,"sheet_y":2,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB","sheet_x":24,"sheet_y":3},"1F3FC":{"unified":"1F470-1F3FC","sheet_x":24,"sheet_y":4},"1F3FD":{"unified":"1F470-1F3FD","sheet_x":24,"sheet_y":5},"1F3FE":{"unified":"1F470-1F3FE","sheet_x":24,"sheet_y":6},"1F3FF":{"unified":"1F470-1F3FF","sheet_x":24,"sheet_y":7}}},{"unified":"1F471-200D-2640-FE0F","sheet_x":24,"sheet_y":8,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB-200D-2640-FE0F","sheet_x":24,"sheet_y":9},"1F3FC":{"unified":"1F471-1F3FC-200D-2640-FE0F","sheet_x":24,"sheet_y":10},"1F3FD":{"unified":"1F471-1F3FD-200D-2640-FE0F","sheet_x":24,"sheet_y":11},"1F3FE":{"unified":"1F471-1F3FE-200D-2640-FE0F","sheet_x":24,"sheet_y":12},"1F3FF":{"unified":"1F471-1F3FF-200D-2640-FE0F","sheet_x":24,"sheet_y":13}}},{"unified":"1F471-200D-2642-FE0F","sheet_x":24,"sheet_y":14,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB-200D-2642-FE0F","sheet_x":24,"sheet_y":15},"1F3FC":{"unified":"1F471-1F3FC-200D-2642-FE0F","sheet_x":24,"sheet_y":16},"1F3FD":{"unified":"1F471-1F3FD-200D-2642-FE0F","sheet_x":24,"sheet_y":17},"1F3FE":{"unified":"1F471-1F3FE-200D-2642-FE0F","sheet_x":24,"sheet_y":18},"1F3FF":{"unified":"1F471-1F3FF-200D-2642-FE0F","sheet_x":24,"sheet_y":19}}},{"unified":"1F471","sheet_x":24,"sheet_y":20,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB","sheet_x":24,"sheet_y":21},"1F3FC":{"unified":"1F471-1F3FC","sheet_x":24,"sheet_y":22},"1F3FD":{"unified":"1F471-1F3FD","sheet_x":24,"sheet_y":23},"1F3FE":{"unified":"1F471-1F3FE","sheet_x":24,"sheet_y":24},"1F3FF":{"unified":"1F471-1F3FF","sheet_x":24,"sheet_y":25}}},{"unified":"1F472","sheet_x":24,"sheet_y":26,"skin_variations":{"1F3FB":{"unified":"1F472-1F3FB","sheet_x":24,"sheet_y":27},"1F3FC":{"unified":"1F472-1F3FC","sheet_x":24,"sheet_y":28},"1F3FD":{"unified":"1F472-1F3FD","sheet_x":24,"sheet_y":29},"1F3FE":{"unified":"1F472-1F3FE","sheet_x":24,"sheet_y":30},"1F3FF":{"unified":"1F472-1F3FF","sheet_x":24,"sheet_y":31}}},{"unified":"1F473-200D-2640-FE0F","sheet_x":24,"sheet_y":32,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB-200D-2640-FE0F","sheet_x":24,"sheet_y":33},"1F3FC":{"unified":"1F473-1F3FC-200D-2640-FE0F","sheet_x":24,"sheet_y":34},"1F3FD":{"unified":"1F473-1F3FD-200D-2640-FE0F","sheet_x":24,"sheet_y":35},"1F3FE":{"unified":"1F473-1F3FE-200D-2640-FE0F","sheet_x":24,"sheet_y":36},"1F3FF":{"unified":"1F473-1F3FF-200D-2640-FE0F","sheet_x":24,"sheet_y":37}}},{"unified":"1F473-200D-2642-FE0F","sheet_x":24,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB-200D-2642-FE0F","sheet_x":24,"sheet_y":39},"1F3FC":{"unified":"1F473-1F3FC-200D-2642-FE0F","sheet_x":24,"sheet_y":40},"1F3FD":{"unified":"1F473-1F3FD-200D-2642-FE0F","sheet_x":24,"sheet_y":41},"1F3FE":{"unified":"1F473-1F3FE-200D-2642-FE0F","sheet_x":24,"sheet_y":42},"1F3FF":{"unified":"1F473-1F3FF-200D-2642-FE0F","sheet_x":24,"sheet_y":43}}},{"unified":"1F473","sheet_x":24,"sheet_y":44,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB","sheet_x":24,"sheet_y":45},"1F3FC":{"unified":"1F473-1F3FC","sheet_x":24,"sheet_y":46},"1F3FD":{"unified":"1F473-1F3FD","sheet_x":24,"sheet_y":47},"1F3FE":{"unified":"1F473-1F3FE","sheet_x":24,"sheet_y":48},"1F3FF":{"unified":"1F473-1F3FF","sheet_x":24,"sheet_y":49}}},{"unified":"1F474","sheet_x":24,"sheet_y":50,"skin_variations":{"1F3FB":{"unified":"1F474-1F3FB","sheet_x":24,"sheet_y":51},"1F3FC":{"unified":"1F474-1F3FC","sheet_x":24,"sheet_y":52},"1F3FD":{"unified":"1F474-1F3FD","sheet_x":24,"sheet_y":53},"1F3FE":{"unified":"1F474-1F3FE","sheet_x":24,"sheet_y":54},"1F3FF":{"unified":"1F474-1F3FF","sheet_x":24,"sheet_y":55}}},{"unified":"1F475","sheet_x":24,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"1F475-1F3FB","sheet_x":24,"sheet_y":57},"1F3FC":{"unified":"1F475-1F3FC","sheet_x":24,"sheet_y":58},"1F3FD":{"unified":"1F475-1F3FD","sheet_x":24,"sheet_y":59},"1F3FE":{"unified":"1F475-1F3FE","sheet_x":24,"sheet_y":60},"1F3FF":{"unified":"1F475-1F3FF","sheet_x":24,"sheet_y":61}}},{"unified":"1F476","sheet_x":25,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"1F476-1F3FB","sheet_x":25,"sheet_y":1},"1F3FC":{"unified":"1F476-1F3FC","sheet_x":25,"sheet_y":2},"1F3FD":{"unified":"1F476-1F3FD","sheet_x":25,"sheet_y":3},"1F3FE":{"unified":"1F476-1F3FE","sheet_x":25,"sheet_y":4},"1F3FF":{"unified":"1F476-1F3FF","sheet_x":25,"sheet_y":5}}},{"unified":"1F477-200D-2640-FE0F","sheet_x":25,"sheet_y":6,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB-200D-2640-FE0F","sheet_x":25,"sheet_y":7},"1F3FC":{"unified":"1F477-1F3FC-200D-2640-FE0F","sheet_x":25,"sheet_y":8},"1F3FD":{"unified":"1F477-1F3FD-200D-2640-FE0F","sheet_x":25,"sheet_y":9},"1F3FE":{"unified":"1F477-1F3FE-200D-2640-FE0F","sheet_x":25,"sheet_y":10},"1F3FF":{"unified":"1F477-1F3FF-200D-2640-FE0F","sheet_x":25,"sheet_y":11}}},{"unified":"1F477-200D-2642-FE0F","sheet_x":25,"sheet_y":12,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB-200D-2642-FE0F","sheet_x":25,"sheet_y":13},"1F3FC":{"unified":"1F477-1F3FC-200D-2642-FE0F","sheet_x":25,"sheet_y":14},"1F3FD":{"unified":"1F477-1F3FD-200D-2642-FE0F","sheet_x":25,"sheet_y":15},"1F3FE":{"unified":"1F477-1F3FE-200D-2642-FE0F","sheet_x":25,"sheet_y":16},"1F3FF":{"unified":"1F477-1F3FF-200D-2642-FE0F","sheet_x":25,"sheet_y":17}}},{"unified":"1F477","sheet_x":25,"sheet_y":18,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB","sheet_x":25,"sheet_y":19},"1F3FC":{"unified":"1F477-1F3FC","sheet_x":25,"sheet_y":20},"1F3FD":{"unified":"1F477-1F3FD","sheet_x":25,"sheet_y":21},"1F3FE":{"unified":"1F477-1F3FE","sheet_x":25,"sheet_y":22},"1F3FF":{"unified":"1F477-1F3FF","sheet_x":25,"sheet_y":23}}},{"unified":"1F478","sheet_x":25,"sheet_y":24,"skin_variations":{"1F3FB":{"unified":"1F478-1F3FB","sheet_x":25,"sheet_y":25},"1F3FC":{"unified":"1F478-1F3FC","sheet_x":25,"sheet_y":26},"1F3FD":{"unified":"1F478-1F3FD","sheet_x":25,"sheet_y":27},"1F3FE":{"unified":"1F478-1F3FE","sheet_x":25,"sheet_y":28},"1F3FF":{"unified":"1F478-1F3FF","sheet_x":25,"sheet_y":29}}},{"unified":"1F479","sheet_x":25,"sheet_y":30,"skin_variations":{}},{"unified":"1F47A","sheet_x":25,"sheet_y":31,"skin_variations":{}},{"unified":"1F47B","sheet_x":25,"sheet_y":32,"skin_variations":{}},{"unified":"1F47C","sheet_x":25,"sheet_y":33,"skin_variations":{"1F3FB":{"unified":"1F47C-1F3FB","sheet_x":25,"sheet_y":34},"1F3FC":{"unified":"1F47C-1F3FC","sheet_x":25,"sheet_y":35},"1F3FD":{"unified":"1F47C-1F3FD","sheet_x":25,"sheet_y":36},"1F3FE":{"unified":"1F47C-1F3FE","sheet_x":25,"sheet_y":37},"1F3FF":{"unified":"1F47C-1F3FF","sheet_x":25,"sheet_y":38}}},{"unified":"1F47D","sheet_x":25,"sheet_y":39,"skin_variations":{}},{"unified":"1F47E","sheet_x":25,"sheet_y":40,"skin_variations":{}},{"unified":"1F47F","sheet_x":25,"sheet_y":41,"skin_variations":{}},{"unified":"1F480","sheet_x":25,"sheet_y":42,"skin_variations":{}},{"unified":"1F481-200D-2640-FE0F","sheet_x":25,"sheet_y":43,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB-200D-2640-FE0F","sheet_x":25,"sheet_y":44},"1F3FC":{"unified":"1F481-1F3FC-200D-2640-FE0F","sheet_x":25,"sheet_y":45},"1F3FD":{"unified":"1F481-1F3FD-200D-2640-FE0F","sheet_x":25,"sheet_y":46},"1F3FE":{"unified":"1F481-1F3FE-200D-2640-FE0F","sheet_x":25,"sheet_y":47},"1F3FF":{"unified":"1F481-1F3FF-200D-2640-FE0F","sheet_x":25,"sheet_y":48}}},{"unified":"1F481-200D-2642-FE0F","sheet_x":25,"sheet_y":49,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB-200D-2642-FE0F","sheet_x":25,"sheet_y":50},"1F3FC":{"unified":"1F481-1F3FC-200D-2642-FE0F","sheet_x":25,"sheet_y":51},"1F3FD":{"unified":"1F481-1F3FD-200D-2642-FE0F","sheet_x":25,"sheet_y":52},"1F3FE":{"unified":"1F481-1F3FE-200D-2642-FE0F","sheet_x":25,"sheet_y":53},"1F3FF":{"unified":"1F481-1F3FF-200D-2642-FE0F","sheet_x":25,"sheet_y":54}}},{"unified":"1F481","sheet_x":25,"sheet_y":55,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB","sheet_x":25,"sheet_y":56},"1F3FC":{"unified":"1F481-1F3FC","sheet_x":25,"sheet_y":57},"1F3FD":{"unified":"1F481-1F3FD","sheet_x":25,"sheet_y":58},"1F3FE":{"unified":"1F481-1F3FE","sheet_x":25,"sheet_y":59},"1F3FF":{"unified":"1F481-1F3FF","sheet_x":25,"sheet_y":60}}},{"unified":"1F482-200D-2640-FE0F","sheet_x":25,"sheet_y":61,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB-200D-2640-FE0F","sheet_x":26,"sheet_y":0},"1F3FC":{"unified":"1F482-1F3FC-200D-2640-FE0F","sheet_x":26,"sheet_y":1},"1F3FD":{"unified":"1F482-1F3FD-200D-2640-FE0F","sheet_x":26,"sheet_y":2},"1F3FE":{"unified":"1F482-1F3FE-200D-2640-FE0F","sheet_x":26,"sheet_y":3},"1F3FF":{"unified":"1F482-1F3FF-200D-2640-FE0F","sheet_x":26,"sheet_y":4}}},{"unified":"1F482-200D-2642-FE0F","sheet_x":26,"sheet_y":5,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB-200D-2642-FE0F","sheet_x":26,"sheet_y":6},"1F3FC":{"unified":"1F482-1F3FC-200D-2642-FE0F","sheet_x":26,"sheet_y":7},"1F3FD":{"unified":"1F482-1F3FD-200D-2642-FE0F","sheet_x":26,"sheet_y":8},"1F3FE":{"unified":"1F482-1F3FE-200D-2642-FE0F","sheet_x":26,"sheet_y":9},"1F3FF":{"unified":"1F482-1F3FF-200D-2642-FE0F","sheet_x":26,"sheet_y":10}}},{"unified":"1F482","sheet_x":26,"sheet_y":11,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB","sheet_x":26,"sheet_y":12},"1F3FC":{"unified":"1F482-1F3FC","sheet_x":26,"sheet_y":13},"1F3FD":{"unified":"1F482-1F3FD","sheet_x":26,"sheet_y":14},"1F3FE":{"unified":"1F482-1F3FE","sheet_x":26,"sheet_y":15},"1F3FF":{"unified":"1F482-1F3FF","sheet_x":26,"sheet_y":16}}},{"unified":"1F483","sheet_x":26,"sheet_y":17,"skin_variations":{"1F3FB":{"unified":"1F483-1F3FB","sheet_x":26,"sheet_y":18},"1F3FC":{"unified":"1F483-1F3FC","sheet_x":26,"sheet_y":19},"1F3FD":{"unified":"1F483-1F3FD","sheet_x":26,"sheet_y":20},"1F3FE":{"unified":"1F483-1F3FE","sheet_x":26,"sheet_y":21},"1F3FF":{"unified":"1F483-1F3FF","sheet_x":26,"sheet_y":22}}},{"unified":"1F484","sheet_x":26,"sheet_y":23,"skin_variations":{}},{"unified":"1F485","sheet_x":26,"sheet_y":24,"skin_variations":{"1F3FB":{"unified":"1F485-1F3FB","sheet_x":26,"sheet_y":25},"1F3FC":{"unified":"1F485-1F3FC","sheet_x":26,"sheet_y":26},"1F3FD":{"unified":"1F485-1F3FD","sheet_x":26,"sheet_y":27},"1F3FE":{"unified":"1F485-1F3FE","sheet_x":26,"sheet_y":28},"1F3FF":{"unified":"1F485-1F3FF","sheet_x":26,"sheet_y":29}}},{"unified":"1F486-200D-2640-FE0F","sheet_x":26,"sheet_y":30,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB-200D-2640-FE0F","sheet_x":26,"sheet_y":31},"1F3FC":{"unified":"1F486-1F3FC-200D-2640-FE0F","sheet_x":26,"sheet_y":32},"1F3FD":{"unified":"1F486-1F3FD-200D-2640-FE0F","sheet_x":26,"sheet_y":33},"1F3FE":{"unified":"1F486-1F3FE-200D-2640-FE0F","sheet_x":26,"sheet_y":34},"1F3FF":{"unified":"1F486-1F3FF-200D-2640-FE0F","sheet_x":26,"sheet_y":35}}},{"unified":"1F486-200D-2642-FE0F","sheet_x":26,"sheet_y":36,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB-200D-2642-FE0F","sheet_x":26,"sheet_y":37},"1F3FC":{"unified":"1F486-1F3FC-200D-2642-FE0F","sheet_x":26,"sheet_y":38},"1F3FD":{"unified":"1F486-1F3FD-200D-2642-FE0F","sheet_x":26,"sheet_y":39},"1F3FE":{"unified":"1F486-1F3FE-200D-2642-FE0F","sheet_x":26,"sheet_y":40},"1F3FF":{"unified":"1F486-1F3FF-200D-2642-FE0F","sheet_x":26,"sheet_y":41}}},{"unified":"1F486","sheet_x":26,"sheet_y":42,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB","sheet_x":26,"sheet_y":43},"1F3FC":{"unified":"1F486-1F3FC","sheet_x":26,"sheet_y":44},"1F3FD":{"unified":"1F486-1F3FD","sheet_x":26,"sheet_y":45},"1F3FE":{"unified":"1F486-1F3FE","sheet_x":26,"sheet_y":46},"1F3FF":{"unified":"1F486-1F3FF","sheet_x":26,"sheet_y":47}}},{"unified":"1F487-200D-2640-FE0F","sheet_x":26,"sheet_y":48,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB-200D-2640-FE0F","sheet_x":26,"sheet_y":49},"1F3FC":{"unified":"1F487-1F3FC-200D-2640-FE0F","sheet_x":26,"sheet_y":50},"1F3FD":{"unified":"1F487-1F3FD-200D-2640-FE0F","sheet_x":26,"sheet_y":51},"1F3FE":{"unified":"1F487-1F3FE-200D-2640-FE0F","sheet_x":26,"sheet_y":52},"1F3FF":{"unified":"1F487-1F3FF-200D-2640-FE0F","sheet_x":26,"sheet_y":53}}},{"unified":"1F487-200D-2642-FE0F","sheet_x":26,"sheet_y":54,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB-200D-2642-FE0F","sheet_x":26,"sheet_y":55},"1F3FC":{"unified":"1F487-1F3FC-200D-2642-FE0F","sheet_x":26,"sheet_y":56},"1F3FD":{"unified":"1F487-1F3FD-200D-2642-FE0F","sheet_x":26,"sheet_y":57},"1F3FE":{"unified":"1F487-1F3FE-200D-2642-FE0F","sheet_x":26,"sheet_y":58},"1F3FF":{"unified":"1F487-1F3FF-200D-2642-FE0F","sheet_x":26,"sheet_y":59}}},{"unified":"1F487","sheet_x":26,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB","sheet_x":26,"sheet_y":61},"1F3FC":{"unified":"1F487-1F3FC","sheet_x":27,"sheet_y":0},"1F3FD":{"unified":"1F487-1F3FD","sheet_x":27,"sheet_y":1},"1F3FE":{"unified":"1F487-1F3FE","sheet_x":27,"sheet_y":2},"1F3FF":{"unified":"1F487-1F3FF","sheet_x":27,"sheet_y":3}}},{"unified":"1F488","sheet_x":27,"sheet_y":4,"skin_variations":{}},{"unified":"1F489","sheet_x":27,"sheet_y":5,"skin_variations":{}},{"unified":"1F48A","sheet_x":27,"sheet_y":6,"skin_variations":{}},{"unified":"1F48B","sheet_x":27,"sheet_y":7,"skin_variations":{}},{"unified":"1F48C","sheet_x":27,"sheet_y":8,"skin_variations":{}},{"unified":"1F48D","sheet_x":27,"sheet_y":9,"skin_variations":{}},{"unified":"1F48E","sheet_x":27,"sheet_y":10,"skin_variations":{}},{"unified":"1F48F","sheet_x":27,"sheet_y":11,"skin_variations":{"1F3FB":{"unified":"1F48F-1F3FB","sheet_x":27,"sheet_y":12},"1F3FC":{"unified":"1F48F-1F3FC","sheet_x":27,"sheet_y":13},"1F3FD":{"unified":"1F48F-1F3FD","sheet_x":27,"sheet_y":14},"1F3FE":{"unified":"1F48F-1F3FE","sheet_x":27,"sheet_y":15},"1F3FF":{"unified":"1F48F-1F3FF","sheet_x":27,"sheet_y":16},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","sheet_x":27,"sheet_y":17},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","sheet_x":27,"sheet_y":18},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","sheet_x":27,"sheet_y":19},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","sheet_x":27,"sheet_y":20},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","sheet_x":27,"sheet_y":21},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","sheet_x":27,"sheet_y":22},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","sheet_x":27,"sheet_y":23},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","sheet_x":27,"sheet_y":24},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","sheet_x":27,"sheet_y":25},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","sheet_x":27,"sheet_y":26},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","sheet_x":27,"sheet_y":27},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","sheet_x":27,"sheet_y":28},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","sheet_x":27,"sheet_y":29},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","sheet_x":27,"sheet_y":30},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","sheet_x":27,"sheet_y":31},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","sheet_x":27,"sheet_y":32},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","sheet_x":27,"sheet_y":33},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","sheet_x":27,"sheet_y":34},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","sheet_x":27,"sheet_y":35},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","sheet_x":27,"sheet_y":36}}},{"unified":"1F490","sheet_x":27,"sheet_y":37,"skin_variations":{}},{"unified":"1F491","sheet_x":27,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"1F491-1F3FB","sheet_x":27,"sheet_y":39},"1F3FC":{"unified":"1F491-1F3FC","sheet_x":27,"sheet_y":40},"1F3FD":{"unified":"1F491-1F3FD","sheet_x":27,"sheet_y":41},"1F3FE":{"unified":"1F491-1F3FE","sheet_x":27,"sheet_y":42},"1F3FF":{"unified":"1F491-1F3FF","sheet_x":27,"sheet_y":43},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FC","sheet_x":27,"sheet_y":44},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FD","sheet_x":27,"sheet_y":45},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FE","sheet_x":27,"sheet_y":46},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FF","sheet_x":27,"sheet_y":47},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FB","sheet_x":27,"sheet_y":48},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FD","sheet_x":27,"sheet_y":49},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FE","sheet_x":27,"sheet_y":50},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FF","sheet_x":27,"sheet_y":51},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FB","sheet_x":27,"sheet_y":52},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FC","sheet_x":27,"sheet_y":53},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FE","sheet_x":27,"sheet_y":54},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FF","sheet_x":27,"sheet_y":55},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FB","sheet_x":27,"sheet_y":56},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FC","sheet_x":27,"sheet_y":57},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FD","sheet_x":27,"sheet_y":58},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FF","sheet_x":27,"sheet_y":59},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FB","sheet_x":27,"sheet_y":60},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FC","sheet_x":27,"sheet_y":61},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FD","sheet_x":28,"sheet_y":0},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FE","sheet_x":28,"sheet_y":1}}},{"unified":"1F492","sheet_x":28,"sheet_y":2,"skin_variations":{}},{"unified":"1F493","sheet_x":28,"sheet_y":3,"skin_variations":{}},{"unified":"1F494","sheet_x":28,"sheet_y":4,"skin_variations":{}},{"unified":"1F495","sheet_x":28,"sheet_y":5,"skin_variations":{}},{"unified":"1F496","sheet_x":28,"sheet_y":6,"skin_variations":{}},{"unified":"1F497","sheet_x":28,"sheet_y":7,"skin_variations":{}},{"unified":"1F498","sheet_x":28,"sheet_y":8,"skin_variations":{}},{"unified":"1F499","sheet_x":28,"sheet_y":9,"skin_variations":{}},{"unified":"1F49A","sheet_x":28,"sheet_y":10,"skin_variations":{}},{"unified":"1F49B","sheet_x":28,"sheet_y":11,"skin_variations":{}},{"unified":"1F49C","sheet_x":28,"sheet_y":12,"skin_variations":{}},{"unified":"1F49D","sheet_x":28,"sheet_y":13,"skin_variations":{}},{"unified":"1F49E","sheet_x":28,"sheet_y":14,"skin_variations":{}},{"unified":"1F49F","sheet_x":28,"sheet_y":15,"skin_variations":{}},{"unified":"1F4A0","sheet_x":28,"sheet_y":16,"skin_variations":{}},{"unified":"1F4A1","sheet_x":28,"sheet_y":17,"skin_variations":{}},{"unified":"1F4A2","sheet_x":28,"sheet_y":18,"skin_variations":{}},{"unified":"1F4A3","sheet_x":28,"sheet_y":19,"skin_variations":{}},{"unified":"1F4A4","sheet_x":28,"sheet_y":20,"skin_variations":{}},{"unified":"1F4A5","sheet_x":28,"sheet_y":21,"skin_variations":{}},{"unified":"1F4A6","sheet_x":28,"sheet_y":22,"skin_variations":{}},{"unified":"1F4A7","sheet_x":28,"sheet_y":23,"skin_variations":{}},{"unified":"1F4A8","sheet_x":28,"sheet_y":24,"skin_variations":{}},{"unified":"1F4A9","sheet_x":28,"sheet_y":25,"skin_variations":{}},{"unified":"1F4AA","sheet_x":28,"sheet_y":26,"skin_variations":{"1F3FB":{"unified":"1F4AA-1F3FB","sheet_x":28,"sheet_y":27},"1F3FC":{"unified":"1F4AA-1F3FC","sheet_x":28,"sheet_y":28},"1F3FD":{"unified":"1F4AA-1F3FD","sheet_x":28,"sheet_y":29},"1F3FE":{"unified":"1F4AA-1F3FE","sheet_x":28,"sheet_y":30},"1F3FF":{"unified":"1F4AA-1F3FF","sheet_x":28,"sheet_y":31}}},{"unified":"1F4AB","sheet_x":28,"sheet_y":32,"skin_variations":{}},{"unified":"1F4AC","sheet_x":28,"sheet_y":33,"skin_variations":{}},{"unified":"1F4AD","sheet_x":28,"sheet_y":34,"skin_variations":{}},{"unified":"1F4AE","sheet_x":28,"sheet_y":35,"skin_variations":{}},{"unified":"1F4AF","sheet_x":28,"sheet_y":36,"skin_variations":{}},{"unified":"1F4B0","sheet_x":28,"sheet_y":37,"skin_variations":{}},{"unified":"1F4B1","sheet_x":28,"sheet_y":38,"skin_variations":{}},{"unified":"1F4B2","sheet_x":28,"sheet_y":39,"skin_variations":{}},{"unified":"1F4B3","sheet_x":28,"sheet_y":40,"skin_variations":{}},{"unified":"1F4B4","sheet_x":28,"sheet_y":41,"skin_variations":{}},{"unified":"1F4B5","sheet_x":28,"sheet_y":42,"skin_variations":{}},{"unified":"1F4B6","sheet_x":28,"sheet_y":43,"skin_variations":{}},{"unified":"1F4B7","sheet_x":28,"sheet_y":44,"skin_variations":{}},{"unified":"1F4B8","sheet_x":28,"sheet_y":45,"skin_variations":{}},{"unified":"1F4B9","sheet_x":28,"sheet_y":46,"skin_variations":{}},{"unified":"1F4BA","sheet_x":28,"sheet_y":47,"skin_variations":{}},{"unified":"1F4BB","sheet_x":28,"sheet_y":48,"skin_variations":{}},{"unified":"1F4BC","sheet_x":28,"sheet_y":49,"skin_variations":{}},{"unified":"1F4BD","sheet_x":28,"sheet_y":50,"skin_variations":{}},{"unified":"1F4BE","sheet_x":28,"sheet_y":51,"skin_variations":{}},{"unified":"1F4BF","sheet_x":28,"sheet_y":52,"skin_variations":{}},{"unified":"1F4C0","sheet_x":28,"sheet_y":53,"skin_variations":{}},{"unified":"1F4C1","sheet_x":28,"sheet_y":54,"skin_variations":{}},{"unified":"1F4C2","sheet_x":28,"sheet_y":55,"skin_variations":{}},{"unified":"1F4C3","sheet_x":28,"sheet_y":56,"skin_variations":{}},{"unified":"1F4C4","sheet_x":28,"sheet_y":57,"skin_variations":{}},{"unified":"1F4C5","sheet_x":28,"sheet_y":58,"skin_variations":{}},{"unified":"1F4C6","sheet_x":28,"sheet_y":59,"skin_variations":{}},{"unified":"1F4C7","sheet_x":28,"sheet_y":60,"skin_variations":{}},{"unified":"1F4C8","sheet_x":28,"sheet_y":61,"skin_variations":{}},{"unified":"1F4C9","sheet_x":29,"sheet_y":0,"skin_variations":{}},{"unified":"1F4CA","sheet_x":29,"sheet_y":1,"skin_variations":{}},{"unified":"1F4CB","sheet_x":29,"sheet_y":2,"skin_variations":{}},{"unified":"1F4CC","sheet_x":29,"sheet_y":3,"skin_variations":{}},{"unified":"1F4CD","sheet_x":29,"sheet_y":4,"skin_variations":{}},{"unified":"1F4CE","sheet_x":29,"sheet_y":5,"skin_variations":{}},{"unified":"1F4CF","sheet_x":29,"sheet_y":6,"skin_variations":{}},{"unified":"1F4D0","sheet_x":29,"sheet_y":7,"skin_variations":{}},{"unified":"1F4D1","sheet_x":29,"sheet_y":8,"skin_variations":{}},{"unified":"1F4D2","sheet_x":29,"sheet_y":9,"skin_variations":{}},{"unified":"1F4D3","sheet_x":29,"sheet_y":10,"skin_variations":{}},{"unified":"1F4D4","sheet_x":29,"sheet_y":11,"skin_variations":{}},{"unified":"1F4D5","sheet_x":29,"sheet_y":12,"skin_variations":{}},{"unified":"1F4D6","sheet_x":29,"sheet_y":13,"skin_variations":{}},{"unified":"1F4D7","sheet_x":29,"sheet_y":14,"skin_variations":{}},{"unified":"1F4D8","sheet_x":29,"sheet_y":15,"skin_variations":{}},{"unified":"1F4D9","sheet_x":29,"sheet_y":16,"skin_variations":{}},{"unified":"1F4DA","sheet_x":29,"sheet_y":17,"skin_variations":{}},{"unified":"1F4DB","sheet_x":29,"sheet_y":18,"skin_variations":{}},{"unified":"1F4DC","sheet_x":29,"sheet_y":19,"skin_variations":{}},{"unified":"1F4DD","sheet_x":29,"sheet_y":20,"skin_variations":{}},{"unified":"1F4DE","sheet_x":29,"sheet_y":21,"skin_variations":{}},{"unified":"1F4DF","sheet_x":29,"sheet_y":22,"skin_variations":{}},{"unified":"1F4E0","sheet_x":29,"sheet_y":23,"skin_variations":{}},{"unified":"1F4E1","sheet_x":29,"sheet_y":24,"skin_variations":{}},{"unified":"1F4E2","sheet_x":29,"sheet_y":25,"skin_variations":{}},{"unified":"1F4E3","sheet_x":29,"sheet_y":26,"skin_variations":{}},{"unified":"1F4E4","sheet_x":29,"sheet_y":27,"skin_variations":{}},{"unified":"1F4E5","sheet_x":29,"sheet_y":28,"skin_variations":{}},{"unified":"1F4E6","sheet_x":29,"sheet_y":29,"skin_variations":{}},{"unified":"1F4E7","sheet_x":29,"sheet_y":30,"skin_variations":{}},{"unified":"1F4E8","sheet_x":29,"sheet_y":31,"skin_variations":{}},{"unified":"1F4E9","sheet_x":29,"sheet_y":32,"skin_variations":{}},{"unified":"1F4EA","sheet_x":29,"sheet_y":33,"skin_variations":{}},{"unified":"1F4EB","sheet_x":29,"sheet_y":34,"skin_variations":{}},{"unified":"1F4EC","sheet_x":29,"sheet_y":35,"skin_variations":{}},{"unified":"1F4ED","sheet_x":29,"sheet_y":36,"skin_variations":{}},{"unified":"1F4EE","sheet_x":29,"sheet_y":37,"skin_variations":{}},{"unified":"1F4EF","sheet_x":29,"sheet_y":38,"skin_variations":{}},{"unified":"1F4F0","sheet_x":29,"sheet_y":39,"skin_variations":{}},{"unified":"1F4F1","sheet_x":29,"sheet_y":40,"skin_variations":{}},{"unified":"1F4F2","sheet_x":29,"sheet_y":41,"skin_variations":{}},{"unified":"1F4F3","sheet_x":29,"sheet_y":42,"skin_variations":{}},{"unified":"1F4F4","sheet_x":29,"sheet_y":43,"skin_variations":{}},{"unified":"1F4F5","sheet_x":29,"sheet_y":44,"skin_variations":{}},{"unified":"1F4F6","sheet_x":29,"sheet_y":45,"skin_variations":{}},{"unified":"1F4F7","sheet_x":29,"sheet_y":46,"skin_variations":{}},{"unified":"1F4F8","sheet_x":29,"sheet_y":47,"skin_variations":{}},{"unified":"1F4F9","sheet_x":29,"sheet_y":48,"skin_variations":{}},{"unified":"1F4FA","sheet_x":29,"sheet_y":49,"skin_variations":{}},{"unified":"1F4FB","sheet_x":29,"sheet_y":50,"skin_variations":{}},{"unified":"1F4FC","sheet_x":29,"sheet_y":51,"skin_variations":{}},{"unified":"1F4FD-FE0F","sheet_x":29,"sheet_y":52,"skin_variations":{}},{"unified":"1F4FF","sheet_x":29,"sheet_y":53,"skin_variations":{}},{"unified":"1F500","sheet_x":29,"sheet_y":54,"skin_variations":{}},{"unified":"1F501","sheet_x":29,"sheet_y":55,"skin_variations":{}},{"unified":"1F502","sheet_x":29,"sheet_y":56,"skin_variations":{}},{"unified":"1F503","sheet_x":29,"sheet_y":57,"skin_variations":{}},{"unified":"1F504","sheet_x":29,"sheet_y":58,"skin_variations":{}},{"unified":"1F505","sheet_x":29,"sheet_y":59,"skin_variations":{}},{"unified":"1F506","sheet_x":29,"sheet_y":60,"skin_variations":{}},{"unified":"1F507","sheet_x":29,"sheet_y":61,"skin_variations":{}},{"unified":"1F508","sheet_x":30,"sheet_y":0,"skin_variations":{}},{"unified":"1F509","sheet_x":30,"sheet_y":1,"skin_variations":{}},{"unified":"1F50A","sheet_x":30,"sheet_y":2,"skin_variations":{}},{"unified":"1F50B","sheet_x":30,"sheet_y":3,"skin_variations":{}},{"unified":"1F50C","sheet_x":30,"sheet_y":4,"skin_variations":{}},{"unified":"1F50D","sheet_x":30,"sheet_y":5,"skin_variations":{}},{"unified":"1F50E","sheet_x":30,"sheet_y":6,"skin_variations":{}},{"unified":"1F50F","sheet_x":30,"sheet_y":7,"skin_variations":{}},{"unified":"1F510","sheet_x":30,"sheet_y":8,"skin_variations":{}},{"unified":"1F511","sheet_x":30,"sheet_y":9,"skin_variations":{}},{"unified":"1F512","sheet_x":30,"sheet_y":10,"skin_variations":{}},{"unified":"1F513","sheet_x":30,"sheet_y":11,"skin_variations":{}},{"unified":"1F514","sheet_x":30,"sheet_y":12,"skin_variations":{}},{"unified":"1F515","sheet_x":30,"sheet_y":13,"skin_variations":{}},{"unified":"1F516","sheet_x":30,"sheet_y":14,"skin_variations":{}},{"unified":"1F517","sheet_x":30,"sheet_y":15,"skin_variations":{}},{"unified":"1F518","sheet_x":30,"sheet_y":16,"skin_variations":{}},{"unified":"1F519","sheet_x":30,"sheet_y":17,"skin_variations":{}},{"unified":"1F51A","sheet_x":30,"sheet_y":18,"skin_variations":{}},{"unified":"1F51B","sheet_x":30,"sheet_y":19,"skin_variations":{}},{"unified":"1F51C","sheet_x":30,"sheet_y":20,"skin_variations":{}},{"unified":"1F51D","sheet_x":30,"sheet_y":21,"skin_variations":{}},{"unified":"1F51E","sheet_x":30,"sheet_y":22,"skin_variations":{}},{"unified":"1F51F","sheet_x":30,"sheet_y":23,"skin_variations":{}},{"unified":"1F520","sheet_x":30,"sheet_y":24,"skin_variations":{}},{"unified":"1F521","sheet_x":30,"sheet_y":25,"skin_variations":{}},{"unified":"1F522","sheet_x":30,"sheet_y":26,"skin_variations":{}},{"unified":"1F523","sheet_x":30,"sheet_y":27,"skin_variations":{}},{"unified":"1F524","sheet_x":30,"sheet_y":28,"skin_variations":{}},{"unified":"1F525","sheet_x":30,"sheet_y":29,"skin_variations":{}},{"unified":"1F526","sheet_x":30,"sheet_y":30,"skin_variations":{}},{"unified":"1F527","sheet_x":30,"sheet_y":31,"skin_variations":{}},{"unified":"1F528","sheet_x":30,"sheet_y":32,"skin_variations":{}},{"unified":"1F529","sheet_x":30,"sheet_y":33,"skin_variations":{}},{"unified":"1F52A","sheet_x":30,"sheet_y":34,"skin_variations":{}},{"unified":"1F52B","sheet_x":30,"sheet_y":35,"skin_variations":{}},{"unified":"1F52C","sheet_x":30,"sheet_y":36,"skin_variations":{}},{"unified":"1F52D","sheet_x":30,"sheet_y":37,"skin_variations":{}},{"unified":"1F52E","sheet_x":30,"sheet_y":38,"skin_variations":{}},{"unified":"1F52F","sheet_x":30,"sheet_y":39,"skin_variations":{}},{"unified":"1F530","sheet_x":30,"sheet_y":40,"skin_variations":{}},{"unified":"1F531","sheet_x":30,"sheet_y":41,"skin_variations":{}},{"unified":"1F532","sheet_x":30,"sheet_y":42,"skin_variations":{}},{"unified":"1F533","sheet_x":30,"sheet_y":43,"skin_variations":{}},{"unified":"1F534","sheet_x":30,"sheet_y":44,"skin_variations":{}},{"unified":"1F535","sheet_x":30,"sheet_y":45,"skin_variations":{}},{"unified":"1F536","sheet_x":30,"sheet_y":46,"skin_variations":{}},{"unified":"1F537","sheet_x":30,"sheet_y":47,"skin_variations":{}},{"unified":"1F538","sheet_x":30,"sheet_y":48,"skin_variations":{}},{"unified":"1F539","sheet_x":30,"sheet_y":49,"skin_variations":{}},{"unified":"1F53A","sheet_x":30,"sheet_y":50,"skin_variations":{}},{"unified":"1F53B","sheet_x":30,"sheet_y":51,"skin_variations":{}},{"unified":"1F53C","sheet_x":30,"sheet_y":52,"skin_variations":{}},{"unified":"1F53D","sheet_x":30,"sheet_y":53,"skin_variations":{}},{"unified":"1F549-FE0F","sheet_x":30,"sheet_y":54,"skin_variations":{}},{"unified":"1F54A-FE0F","sheet_x":30,"sheet_y":55,"skin_variations":{}},{"unified":"1F54B","sheet_x":30,"sheet_y":56,"skin_variations":{}},{"unified":"1F54C","sheet_x":30,"sheet_y":57,"skin_variations":{}},{"unified":"1F54D","sheet_x":30,"sheet_y":58,"skin_variations":{}},{"unified":"1F54E","sheet_x":30,"sheet_y":59,"skin_variations":{}},{"unified":"1F550","sheet_x":30,"sheet_y":60,"skin_variations":{}},{"unified":"1F551","sheet_x":30,"sheet_y":61,"skin_variations":{}},{"unified":"1F552","sheet_x":31,"sheet_y":0,"skin_variations":{}},{"unified":"1F553","sheet_x":31,"sheet_y":1,"skin_variations":{}},{"unified":"1F554","sheet_x":31,"sheet_y":2,"skin_variations":{}},{"unified":"1F555","sheet_x":31,"sheet_y":3,"skin_variations":{}},{"unified":"1F556","sheet_x":31,"sheet_y":4,"skin_variations":{}},{"unified":"1F557","sheet_x":31,"sheet_y":5,"skin_variations":{}},{"unified":"1F558","sheet_x":31,"sheet_y":6,"skin_variations":{}},{"unified":"1F559","sheet_x":31,"sheet_y":7,"skin_variations":{}},{"unified":"1F55A","sheet_x":31,"sheet_y":8,"skin_variations":{}},{"unified":"1F55B","sheet_x":31,"sheet_y":9,"skin_variations":{}},{"unified":"1F55C","sheet_x":31,"sheet_y":10,"skin_variations":{}},{"unified":"1F55D","sheet_x":31,"sheet_y":11,"skin_variations":{}},{"unified":"1F55E","sheet_x":31,"sheet_y":12,"skin_variations":{}},{"unified":"1F55F","sheet_x":31,"sheet_y":13,"skin_variations":{}},{"unified":"1F560","sheet_x":31,"sheet_y":14,"skin_variations":{}},{"unified":"1F561","sheet_x":31,"sheet_y":15,"skin_variations":{}},{"unified":"1F562","sheet_x":31,"sheet_y":16,"skin_variations":{}},{"unified":"1F563","sheet_x":31,"sheet_y":17,"skin_variations":{}},{"unified":"1F564","sheet_x":31,"sheet_y":18,"skin_variations":{}},{"unified":"1F565","sheet_x":31,"sheet_y":19,"skin_variations":{}},{"unified":"1F566","sheet_x":31,"sheet_y":20,"skin_variations":{}},{"unified":"1F567","sheet_x":31,"sheet_y":21,"skin_variations":{}},{"unified":"1F56F-FE0F","sheet_x":31,"sheet_y":22,"skin_variations":{}},{"unified":"1F570-FE0F","sheet_x":31,"sheet_y":23,"skin_variations":{}},{"unified":"1F573-FE0F","sheet_x":31,"sheet_y":24,"skin_variations":{}},{"unified":"1F574-FE0F","sheet_x":31,"sheet_y":25,"skin_variations":{"1F3FB":{"unified":"1F574-1F3FB","sheet_x":31,"sheet_y":26},"1F3FC":{"unified":"1F574-1F3FC","sheet_x":31,"sheet_y":27},"1F3FD":{"unified":"1F574-1F3FD","sheet_x":31,"sheet_y":28},"1F3FE":{"unified":"1F574-1F3FE","sheet_x":31,"sheet_y":29},"1F3FF":{"unified":"1F574-1F3FF","sheet_x":31,"sheet_y":30}}},{"unified":"1F575-FE0F-200D-2640-FE0F","sheet_x":31,"sheet_y":31,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB-200D-2640-FE0F","sheet_x":31,"sheet_y":32},"1F3FC":{"unified":"1F575-1F3FC-200D-2640-FE0F","sheet_x":31,"sheet_y":33},"1F3FD":{"unified":"1F575-1F3FD-200D-2640-FE0F","sheet_x":31,"sheet_y":34},"1F3FE":{"unified":"1F575-1F3FE-200D-2640-FE0F","sheet_x":31,"sheet_y":35},"1F3FF":{"unified":"1F575-1F3FF-200D-2640-FE0F","sheet_x":31,"sheet_y":36}}},{"unified":"1F575-FE0F-200D-2642-FE0F","sheet_x":31,"sheet_y":37,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB-200D-2642-FE0F","sheet_x":31,"sheet_y":38},"1F3FC":{"unified":"1F575-1F3FC-200D-2642-FE0F","sheet_x":31,"sheet_y":39},"1F3FD":{"unified":"1F575-1F3FD-200D-2642-FE0F","sheet_x":31,"sheet_y":40},"1F3FE":{"unified":"1F575-1F3FE-200D-2642-FE0F","sheet_x":31,"sheet_y":41},"1F3FF":{"unified":"1F575-1F3FF-200D-2642-FE0F","sheet_x":31,"sheet_y":42}}},{"unified":"1F575-FE0F","sheet_x":31,"sheet_y":43,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB","sheet_x":31,"sheet_y":44},"1F3FC":{"unified":"1F575-1F3FC","sheet_x":31,"sheet_y":45},"1F3FD":{"unified":"1F575-1F3FD","sheet_x":31,"sheet_y":46},"1F3FE":{"unified":"1F575-1F3FE","sheet_x":31,"sheet_y":47},"1F3FF":{"unified":"1F575-1F3FF","sheet_x":31,"sheet_y":48}}},{"unified":"1F576-FE0F","sheet_x":31,"sheet_y":49,"skin_variations":{}},{"unified":"1F577-FE0F","sheet_x":31,"sheet_y":50,"skin_variations":{}},{"unified":"1F578-FE0F","sheet_x":31,"sheet_y":51,"skin_variations":{}},{"unified":"1F579-FE0F","sheet_x":31,"sheet_y":52,"skin_variations":{}},{"unified":"1F57A","sheet_x":31,"sheet_y":53,"skin_variations":{"1F3FB":{"unified":"1F57A-1F3FB","sheet_x":31,"sheet_y":54},"1F3FC":{"unified":"1F57A-1F3FC","sheet_x":31,"sheet_y":55},"1F3FD":{"unified":"1F57A-1F3FD","sheet_x":31,"sheet_y":56},"1F3FE":{"unified":"1F57A-1F3FE","sheet_x":31,"sheet_y":57},"1F3FF":{"unified":"1F57A-1F3FF","sheet_x":31,"sheet_y":58}}},{"unified":"1F587-FE0F","sheet_x":31,"sheet_y":59,"skin_variations":{}},{"unified":"1F58A-FE0F","sheet_x":31,"sheet_y":60,"skin_variations":{}},{"unified":"1F58B-FE0F","sheet_x":31,"sheet_y":61,"skin_variations":{}},{"unified":"1F58C-FE0F","sheet_x":32,"sheet_y":0,"skin_variations":{}},{"unified":"1F58D-FE0F","sheet_x":32,"sheet_y":1,"skin_variations":{}},{"unified":"1F590-FE0F","sheet_x":32,"sheet_y":2,"skin_variations":{"1F3FB":{"unified":"1F590-1F3FB","sheet_x":32,"sheet_y":3},"1F3FC":{"unified":"1F590-1F3FC","sheet_x":32,"sheet_y":4},"1F3FD":{"unified":"1F590-1F3FD","sheet_x":32,"sheet_y":5},"1F3FE":{"unified":"1F590-1F3FE","sheet_x":32,"sheet_y":6},"1F3FF":{"unified":"1F590-1F3FF","sheet_x":32,"sheet_y":7}}},{"unified":"1F595","sheet_x":32,"sheet_y":8,"skin_variations":{"1F3FB":{"unified":"1F595-1F3FB","sheet_x":32,"sheet_y":9},"1F3FC":{"unified":"1F595-1F3FC","sheet_x":32,"sheet_y":10},"1F3FD":{"unified":"1F595-1F3FD","sheet_x":32,"sheet_y":11},"1F3FE":{"unified":"1F595-1F3FE","sheet_x":32,"sheet_y":12},"1F3FF":{"unified":"1F595-1F3FF","sheet_x":32,"sheet_y":13}}},{"unified":"1F596","sheet_x":32,"sheet_y":14,"skin_variations":{"1F3FB":{"unified":"1F596-1F3FB","sheet_x":32,"sheet_y":15},"1F3FC":{"unified":"1F596-1F3FC","sheet_x":32,"sheet_y":16},"1F3FD":{"unified":"1F596-1F3FD","sheet_x":32,"sheet_y":17},"1F3FE":{"unified":"1F596-1F3FE","sheet_x":32,"sheet_y":18},"1F3FF":{"unified":"1F596-1F3FF","sheet_x":32,"sheet_y":19}}},{"unified":"1F5A4","sheet_x":32,"sheet_y":20,"skin_variations":{}},{"unified":"1F5A5-FE0F","sheet_x":32,"sheet_y":21,"skin_variations":{}},{"unified":"1F5A8-FE0F","sheet_x":32,"sheet_y":22,"skin_variations":{}},{"unified":"1F5B1-FE0F","sheet_x":32,"sheet_y":23,"skin_variations":{}},{"unified":"1F5B2-FE0F","sheet_x":32,"sheet_y":24,"skin_variations":{}},{"unified":"1F5BC-FE0F","sheet_x":32,"sheet_y":25,"skin_variations":{}},{"unified":"1F5C2-FE0F","sheet_x":32,"sheet_y":26,"skin_variations":{}},{"unified":"1F5C3-FE0F","sheet_x":32,"sheet_y":27,"skin_variations":{}},{"unified":"1F5C4-FE0F","sheet_x":32,"sheet_y":28,"skin_variations":{}},{"unified":"1F5D1-FE0F","sheet_x":32,"sheet_y":29,"skin_variations":{}},{"unified":"1F5D2-FE0F","sheet_x":32,"sheet_y":30,"skin_variations":{}},{"unified":"1F5D3-FE0F","sheet_x":32,"sheet_y":31,"skin_variations":{}},{"unified":"1F5DC-FE0F","sheet_x":32,"sheet_y":32,"skin_variations":{}},{"unified":"1F5DD-FE0F","sheet_x":32,"sheet_y":33,"skin_variations":{}},{"unified":"1F5DE-FE0F","sheet_x":32,"sheet_y":34,"skin_variations":{}},{"unified":"1F5E1-FE0F","sheet_x":32,"sheet_y":35,"skin_variations":{}},{"unified":"1F5E3-FE0F","sheet_x":32,"sheet_y":36,"skin_variations":{}},{"unified":"1F5E8-FE0F","sheet_x":32,"sheet_y":37,"skin_variations":{}},{"unified":"1F5EF-FE0F","sheet_x":32,"sheet_y":38,"skin_variations":{}},{"unified":"1F5F3-FE0F","sheet_x":32,"sheet_y":39,"skin_variations":{}},{"unified":"1F5FA-FE0F","sheet_x":32,"sheet_y":40,"skin_variations":{}},{"unified":"1F5FB","sheet_x":32,"sheet_y":41,"skin_variations":{}},{"unified":"1F5FC","sheet_x":32,"sheet_y":42,"skin_variations":{}},{"unified":"1F5FD","sheet_x":32,"sheet_y":43,"skin_variations":{}},{"unified":"1F5FE","sheet_x":32,"sheet_y":44,"skin_variations":{}},{"unified":"1F5FF","sheet_x":32,"sheet_y":45,"skin_variations":{}},{"unified":"1F600","sheet_x":32,"sheet_y":46,"skin_variations":{}},{"unified":"1F601","sheet_x":32,"sheet_y":47,"skin_variations":{}},{"unified":"1F602","sheet_x":32,"sheet_y":48,"skin_variations":{}},{"unified":"1F603","sheet_x":32,"sheet_y":49,"skin_variations":{}},{"unified":"1F604","sheet_x":32,"sheet_y":50,"skin_variations":{}},{"unified":"1F605","sheet_x":32,"sheet_y":51,"skin_variations":{}},{"unified":"1F606","sheet_x":32,"sheet_y":52,"skin_variations":{}},{"unified":"1F607","sheet_x":32,"sheet_y":53,"skin_variations":{}},{"unified":"1F608","sheet_x":32,"sheet_y":54,"skin_variations":{}},{"unified":"1F609","sheet_x":32,"sheet_y":55,"skin_variations":{}},{"unified":"1F60A","sheet_x":32,"sheet_y":56,"skin_variations":{}},{"unified":"1F60B","sheet_x":32,"sheet_y":57,"skin_variations":{}},{"unified":"1F60C","sheet_x":32,"sheet_y":58,"skin_variations":{}},{"unified":"1F60D","sheet_x":32,"sheet_y":59,"skin_variations":{}},{"unified":"1F60E","sheet_x":32,"sheet_y":60,"skin_variations":{}},{"unified":"1F60F","sheet_x":32,"sheet_y":61,"skin_variations":{}},{"unified":"1F610","sheet_x":33,"sheet_y":0,"skin_variations":{}},{"unified":"1F611","sheet_x":33,"sheet_y":1,"skin_variations":{}},{"unified":"1F612","sheet_x":33,"sheet_y":2,"skin_variations":{}},{"unified":"1F613","sheet_x":33,"sheet_y":3,"skin_variations":{}},{"unified":"1F614","sheet_x":33,"sheet_y":4,"skin_variations":{}},{"unified":"1F615","sheet_x":33,"sheet_y":5,"skin_variations":{}},{"unified":"1F616","sheet_x":33,"sheet_y":6,"skin_variations":{}},{"unified":"1F617","sheet_x":33,"sheet_y":7,"skin_variations":{}},{"unified":"1F618","sheet_x":33,"sheet_y":8,"skin_variations":{}},{"unified":"1F619","sheet_x":33,"sheet_y":9,"skin_variations":{}},{"unified":"1F61A","sheet_x":33,"sheet_y":10,"skin_variations":{}},{"unified":"1F61B","sheet_x":33,"sheet_y":11,"skin_variations":{}},{"unified":"1F61C","sheet_x":33,"sheet_y":12,"skin_variations":{}},{"unified":"1F61D","sheet_x":33,"sheet_y":13,"skin_variations":{}},{"unified":"1F61E","sheet_x":33,"sheet_y":14,"skin_variations":{}},{"unified":"1F61F","sheet_x":33,"sheet_y":15,"skin_variations":{}},{"unified":"1F620","sheet_x":33,"sheet_y":16,"skin_variations":{}},{"unified":"1F621","sheet_x":33,"sheet_y":17,"skin_variations":{}},{"unified":"1F622","sheet_x":33,"sheet_y":18,"skin_variations":{}},{"unified":"1F623","sheet_x":33,"sheet_y":19,"skin_variations":{}},{"unified":"1F624","sheet_x":33,"sheet_y":20,"skin_variations":{}},{"unified":"1F625","sheet_x":33,"sheet_y":21,"skin_variations":{}},{"unified":"1F626","sheet_x":33,"sheet_y":22,"skin_variations":{}},{"unified":"1F627","sheet_x":33,"sheet_y":23,"skin_variations":{}},{"unified":"1F628","sheet_x":33,"sheet_y":24,"skin_variations":{}},{"unified":"1F629","sheet_x":33,"sheet_y":25,"skin_variations":{}},{"unified":"1F62A","sheet_x":33,"sheet_y":26,"skin_variations":{}},{"unified":"1F62B","sheet_x":33,"sheet_y":27,"skin_variations":{}},{"unified":"1F62C","sheet_x":33,"sheet_y":28,"skin_variations":{}},{"unified":"1F62D","sheet_x":33,"sheet_y":29,"skin_variations":{}},{"unified":"1F62E-200D-1F4A8","sheet_x":33,"sheet_y":30,"skin_variations":{}},{"unified":"1F62E","sheet_x":33,"sheet_y":31,"skin_variations":{}},{"unified":"1F62F","sheet_x":33,"sheet_y":32,"skin_variations":{}},{"unified":"1F630","sheet_x":33,"sheet_y":33,"skin_variations":{}},{"unified":"1F631","sheet_x":33,"sheet_y":34,"skin_variations":{}},{"unified":"1F632","sheet_x":33,"sheet_y":35,"skin_variations":{}},{"unified":"1F633","sheet_x":33,"sheet_y":36,"skin_variations":{}},{"unified":"1F634","sheet_x":33,"sheet_y":37,"skin_variations":{}},{"unified":"1F635-200D-1F4AB","sheet_x":33,"sheet_y":38,"skin_variations":{}},{"unified":"1F635","sheet_x":33,"sheet_y":39,"skin_variations":{}},{"unified":"1F636-200D-1F32B-FE0F","sheet_x":33,"sheet_y":40,"skin_variations":{}},{"unified":"1F636","sheet_x":33,"sheet_y":41,"skin_variations":{}},{"unified":"1F637","sheet_x":33,"sheet_y":42,"skin_variations":{}},{"unified":"1F638","sheet_x":33,"sheet_y":43,"skin_variations":{}},{"unified":"1F639","sheet_x":33,"sheet_y":44,"skin_variations":{}},{"unified":"1F63A","sheet_x":33,"sheet_y":45,"skin_variations":{}},{"unified":"1F63B","sheet_x":33,"sheet_y":46,"skin_variations":{}},{"unified":"1F63C","sheet_x":33,"sheet_y":47,"skin_variations":{}},{"unified":"1F63D","sheet_x":33,"sheet_y":48,"skin_variations":{}},{"unified":"1F63E","sheet_x":33,"sheet_y":49,"skin_variations":{}},{"unified":"1F63F","sheet_x":33,"sheet_y":50,"skin_variations":{}},{"unified":"1F640","sheet_x":33,"sheet_y":51,"skin_variations":{}},{"unified":"1F641","sheet_x":33,"sheet_y":52,"skin_variations":{}},{"unified":"1F642-200D-2194-FE0F","sheet_x":33,"sheet_y":53,"skin_variations":{}},{"unified":"1F642-200D-2195-FE0F","sheet_x":33,"sheet_y":54,"skin_variations":{}},{"unified":"1F642","sheet_x":33,"sheet_y":55,"skin_variations":{}},{"unified":"1F643","sheet_x":33,"sheet_y":56,"skin_variations":{}},{"unified":"1F644","sheet_x":33,"sheet_y":57,"skin_variations":{}},{"unified":"1F645-200D-2640-FE0F","sheet_x":33,"sheet_y":58,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB-200D-2640-FE0F","sheet_x":33,"sheet_y":59},"1F3FC":{"unified":"1F645-1F3FC-200D-2640-FE0F","sheet_x":33,"sheet_y":60},"1F3FD":{"unified":"1F645-1F3FD-200D-2640-FE0F","sheet_x":33,"sheet_y":61},"1F3FE":{"unified":"1F645-1F3FE-200D-2640-FE0F","sheet_x":34,"sheet_y":0},"1F3FF":{"unified":"1F645-1F3FF-200D-2640-FE0F","sheet_x":34,"sheet_y":1}}},{"unified":"1F645-200D-2642-FE0F","sheet_x":34,"sheet_y":2,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB-200D-2642-FE0F","sheet_x":34,"sheet_y":3},"1F3FC":{"unified":"1F645-1F3FC-200D-2642-FE0F","sheet_x":34,"sheet_y":4},"1F3FD":{"unified":"1F645-1F3FD-200D-2642-FE0F","sheet_x":34,"sheet_y":5},"1F3FE":{"unified":"1F645-1F3FE-200D-2642-FE0F","sheet_x":34,"sheet_y":6},"1F3FF":{"unified":"1F645-1F3FF-200D-2642-FE0F","sheet_x":34,"sheet_y":7}}},{"unified":"1F645","sheet_x":34,"sheet_y":8,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB","sheet_x":34,"sheet_y":9},"1F3FC":{"unified":"1F645-1F3FC","sheet_x":34,"sheet_y":10},"1F3FD":{"unified":"1F645-1F3FD","sheet_x":34,"sheet_y":11},"1F3FE":{"unified":"1F645-1F3FE","sheet_x":34,"sheet_y":12},"1F3FF":{"unified":"1F645-1F3FF","sheet_x":34,"sheet_y":13}}},{"unified":"1F646-200D-2640-FE0F","sheet_x":34,"sheet_y":14,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB-200D-2640-FE0F","sheet_x":34,"sheet_y":15},"1F3FC":{"unified":"1F646-1F3FC-200D-2640-FE0F","sheet_x":34,"sheet_y":16},"1F3FD":{"unified":"1F646-1F3FD-200D-2640-FE0F","sheet_x":34,"sheet_y":17},"1F3FE":{"unified":"1F646-1F3FE-200D-2640-FE0F","sheet_x":34,"sheet_y":18},"1F3FF":{"unified":"1F646-1F3FF-200D-2640-FE0F","sheet_x":34,"sheet_y":19}}},{"unified":"1F646-200D-2642-FE0F","sheet_x":34,"sheet_y":20,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB-200D-2642-FE0F","sheet_x":34,"sheet_y":21},"1F3FC":{"unified":"1F646-1F3FC-200D-2642-FE0F","sheet_x":34,"sheet_y":22},"1F3FD":{"unified":"1F646-1F3FD-200D-2642-FE0F","sheet_x":34,"sheet_y":23},"1F3FE":{"unified":"1F646-1F3FE-200D-2642-FE0F","sheet_x":34,"sheet_y":24},"1F3FF":{"unified":"1F646-1F3FF-200D-2642-FE0F","sheet_x":34,"sheet_y":25}}},{"unified":"1F646","sheet_x":34,"sheet_y":26,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB","sheet_x":34,"sheet_y":27},"1F3FC":{"unified":"1F646-1F3FC","sheet_x":34,"sheet_y":28},"1F3FD":{"unified":"1F646-1F3FD","sheet_x":34,"sheet_y":29},"1F3FE":{"unified":"1F646-1F3FE","sheet_x":34,"sheet_y":30},"1F3FF":{"unified":"1F646-1F3FF","sheet_x":34,"sheet_y":31}}},{"unified":"1F647-200D-2640-FE0F","sheet_x":34,"sheet_y":32,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB-200D-2640-FE0F","sheet_x":34,"sheet_y":33},"1F3FC":{"unified":"1F647-1F3FC-200D-2640-FE0F","sheet_x":34,"sheet_y":34},"1F3FD":{"unified":"1F647-1F3FD-200D-2640-FE0F","sheet_x":34,"sheet_y":35},"1F3FE":{"unified":"1F647-1F3FE-200D-2640-FE0F","sheet_x":34,"sheet_y":36},"1F3FF":{"unified":"1F647-1F3FF-200D-2640-FE0F","sheet_x":34,"sheet_y":37}}},{"unified":"1F647-200D-2642-FE0F","sheet_x":34,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB-200D-2642-FE0F","sheet_x":34,"sheet_y":39},"1F3FC":{"unified":"1F647-1F3FC-200D-2642-FE0F","sheet_x":34,"sheet_y":40},"1F3FD":{"unified":"1F647-1F3FD-200D-2642-FE0F","sheet_x":34,"sheet_y":41},"1F3FE":{"unified":"1F647-1F3FE-200D-2642-FE0F","sheet_x":34,"sheet_y":42},"1F3FF":{"unified":"1F647-1F3FF-200D-2642-FE0F","sheet_x":34,"sheet_y":43}}},{"unified":"1F647","sheet_x":34,"sheet_y":44,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB","sheet_x":34,"sheet_y":45},"1F3FC":{"unified":"1F647-1F3FC","sheet_x":34,"sheet_y":46},"1F3FD":{"unified":"1F647-1F3FD","sheet_x":34,"sheet_y":47},"1F3FE":{"unified":"1F647-1F3FE","sheet_x":34,"sheet_y":48},"1F3FF":{"unified":"1F647-1F3FF","sheet_x":34,"sheet_y":49}}},{"unified":"1F648","sheet_x":34,"sheet_y":50,"skin_variations":{}},{"unified":"1F649","sheet_x":34,"sheet_y":51,"skin_variations":{}},{"unified":"1F64A","sheet_x":34,"sheet_y":52,"skin_variations":{}},{"unified":"1F64B-200D-2640-FE0F","sheet_x":34,"sheet_y":53,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB-200D-2640-FE0F","sheet_x":34,"sheet_y":54},"1F3FC":{"unified":"1F64B-1F3FC-200D-2640-FE0F","sheet_x":34,"sheet_y":55},"1F3FD":{"unified":"1F64B-1F3FD-200D-2640-FE0F","sheet_x":34,"sheet_y":56},"1F3FE":{"unified":"1F64B-1F3FE-200D-2640-FE0F","sheet_x":34,"sheet_y":57},"1F3FF":{"unified":"1F64B-1F3FF-200D-2640-FE0F","sheet_x":34,"sheet_y":58}}},{"unified":"1F64B-200D-2642-FE0F","sheet_x":34,"sheet_y":59,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB-200D-2642-FE0F","sheet_x":34,"sheet_y":60},"1F3FC":{"unified":"1F64B-1F3FC-200D-2642-FE0F","sheet_x":34,"sheet_y":61},"1F3FD":{"unified":"1F64B-1F3FD-200D-2642-FE0F","sheet_x":35,"sheet_y":0},"1F3FE":{"unified":"1F64B-1F3FE-200D-2642-FE0F","sheet_x":35,"sheet_y":1},"1F3FF":{"unified":"1F64B-1F3FF-200D-2642-FE0F","sheet_x":35,"sheet_y":2}}},{"unified":"1F64B","sheet_x":35,"sheet_y":3,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB","sheet_x":35,"sheet_y":4},"1F3FC":{"unified":"1F64B-1F3FC","sheet_x":35,"sheet_y":5},"1F3FD":{"unified":"1F64B-1F3FD","sheet_x":35,"sheet_y":6},"1F3FE":{"unified":"1F64B-1F3FE","sheet_x":35,"sheet_y":7},"1F3FF":{"unified":"1F64B-1F3FF","sheet_x":35,"sheet_y":8}}},{"unified":"1F64C","sheet_x":35,"sheet_y":9,"skin_variations":{"1F3FB":{"unified":"1F64C-1F3FB","sheet_x":35,"sheet_y":10},"1F3FC":{"unified":"1F64C-1F3FC","sheet_x":35,"sheet_y":11},"1F3FD":{"unified":"1F64C-1F3FD","sheet_x":35,"sheet_y":12},"1F3FE":{"unified":"1F64C-1F3FE","sheet_x":35,"sheet_y":13},"1F3FF":{"unified":"1F64C-1F3FF","sheet_x":35,"sheet_y":14}}},{"unified":"1F64D-200D-2640-FE0F","sheet_x":35,"sheet_y":15,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB-200D-2640-FE0F","sheet_x":35,"sheet_y":16},"1F3FC":{"unified":"1F64D-1F3FC-200D-2640-FE0F","sheet_x":35,"sheet_y":17},"1F3FD":{"unified":"1F64D-1F3FD-200D-2640-FE0F","sheet_x":35,"sheet_y":18},"1F3FE":{"unified":"1F64D-1F3FE-200D-2640-FE0F","sheet_x":35,"sheet_y":19},"1F3FF":{"unified":"1F64D-1F3FF-200D-2640-FE0F","sheet_x":35,"sheet_y":20}}},{"unified":"1F64D-200D-2642-FE0F","sheet_x":35,"sheet_y":21,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB-200D-2642-FE0F","sheet_x":35,"sheet_y":22},"1F3FC":{"unified":"1F64D-1F3FC-200D-2642-FE0F","sheet_x":35,"sheet_y":23},"1F3FD":{"unified":"1F64D-1F3FD-200D-2642-FE0F","sheet_x":35,"sheet_y":24},"1F3FE":{"unified":"1F64D-1F3FE-200D-2642-FE0F","sheet_x":35,"sheet_y":25},"1F3FF":{"unified":"1F64D-1F3FF-200D-2642-FE0F","sheet_x":35,"sheet_y":26}}},{"unified":"1F64D","sheet_x":35,"sheet_y":27,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB","sheet_x":35,"sheet_y":28},"1F3FC":{"unified":"1F64D-1F3FC","sheet_x":35,"sheet_y":29},"1F3FD":{"unified":"1F64D-1F3FD","sheet_x":35,"sheet_y":30},"1F3FE":{"unified":"1F64D-1F3FE","sheet_x":35,"sheet_y":31},"1F3FF":{"unified":"1F64D-1F3FF","sheet_x":35,"sheet_y":32}}},{"unified":"1F64E-200D-2640-FE0F","sheet_x":35,"sheet_y":33,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB-200D-2640-FE0F","sheet_x":35,"sheet_y":34},"1F3FC":{"unified":"1F64E-1F3FC-200D-2640-FE0F","sheet_x":35,"sheet_y":35},"1F3FD":{"unified":"1F64E-1F3FD-200D-2640-FE0F","sheet_x":35,"sheet_y":36},"1F3FE":{"unified":"1F64E-1F3FE-200D-2640-FE0F","sheet_x":35,"sheet_y":37},"1F3FF":{"unified":"1F64E-1F3FF-200D-2640-FE0F","sheet_x":35,"sheet_y":38}}},{"unified":"1F64E-200D-2642-FE0F","sheet_x":35,"sheet_y":39,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB-200D-2642-FE0F","sheet_x":35,"sheet_y":40},"1F3FC":{"unified":"1F64E-1F3FC-200D-2642-FE0F","sheet_x":35,"sheet_y":41},"1F3FD":{"unified":"1F64E-1F3FD-200D-2642-FE0F","sheet_x":35,"sheet_y":42},"1F3FE":{"unified":"1F64E-1F3FE-200D-2642-FE0F","sheet_x":35,"sheet_y":43},"1F3FF":{"unified":"1F64E-1F3FF-200D-2642-FE0F","sheet_x":35,"sheet_y":44}}},{"unified":"1F64E","sheet_x":35,"sheet_y":45,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB","sheet_x":35,"sheet_y":46},"1F3FC":{"unified":"1F64E-1F3FC","sheet_x":35,"sheet_y":47},"1F3FD":{"unified":"1F64E-1F3FD","sheet_x":35,"sheet_y":48},"1F3FE":{"unified":"1F64E-1F3FE","sheet_x":35,"sheet_y":49},"1F3FF":{"unified":"1F64E-1F3FF","sheet_x":35,"sheet_y":50}}},{"unified":"1F64F","sheet_x":35,"sheet_y":51,"skin_variations":{"1F3FB":{"unified":"1F64F-1F3FB","sheet_x":35,"sheet_y":52},"1F3FC":{"unified":"1F64F-1F3FC","sheet_x":35,"sheet_y":53},"1F3FD":{"unified":"1F64F-1F3FD","sheet_x":35,"sheet_y":54},"1F3FE":{"unified":"1F64F-1F3FE","sheet_x":35,"sheet_y":55},"1F3FF":{"unified":"1F64F-1F3FF","sheet_x":35,"sheet_y":56}}},{"unified":"1F680","sheet_x":35,"sheet_y":57,"skin_variations":{}},{"unified":"1F681","sheet_x":35,"sheet_y":58,"skin_variations":{}},{"unified":"1F682","sheet_x":35,"sheet_y":59,"skin_variations":{}},{"unified":"1F683","sheet_x":35,"sheet_y":60,"skin_variations":{}},{"unified":"1F684","sheet_x":35,"sheet_y":61,"skin_variations":{}},{"unified":"1F685","sheet_x":36,"sheet_y":0,"skin_variations":{}},{"unified":"1F686","sheet_x":36,"sheet_y":1,"skin_variations":{}},{"unified":"1F687","sheet_x":36,"sheet_y":2,"skin_variations":{}},{"unified":"1F688","sheet_x":36,"sheet_y":3,"skin_variations":{}},{"unified":"1F689","sheet_x":36,"sheet_y":4,"skin_variations":{}},{"unified":"1F68A","sheet_x":36,"sheet_y":5,"skin_variations":{}},{"unified":"1F68B","sheet_x":36,"sheet_y":6,"skin_variations":{}},{"unified":"1F68C","sheet_x":36,"sheet_y":7,"skin_variations":{}},{"unified":"1F68D","sheet_x":36,"sheet_y":8,"skin_variations":{}},{"unified":"1F68E","sheet_x":36,"sheet_y":9,"skin_variations":{}},{"unified":"1F68F","sheet_x":36,"sheet_y":10,"skin_variations":{}},{"unified":"1F690","sheet_x":36,"sheet_y":11,"skin_variations":{}},{"unified":"1F691","sheet_x":36,"sheet_y":12,"skin_variations":{}},{"unified":"1F692","sheet_x":36,"sheet_y":13,"skin_variations":{}},{"unified":"1F693","sheet_x":36,"sheet_y":14,"skin_variations":{}},{"unified":"1F694","sheet_x":36,"sheet_y":15,"skin_variations":{}},{"unified":"1F695","sheet_x":36,"sheet_y":16,"skin_variations":{}},{"unified":"1F696","sheet_x":36,"sheet_y":17,"skin_variations":{}},{"unified":"1F697","sheet_x":36,"sheet_y":18,"skin_variations":{}},{"unified":"1F698","sheet_x":36,"sheet_y":19,"skin_variations":{}},{"unified":"1F699","sheet_x":36,"sheet_y":20,"skin_variations":{}},{"unified":"1F69A","sheet_x":36,"sheet_y":21,"skin_variations":{}},{"unified":"1F69B","sheet_x":36,"sheet_y":22,"skin_variations":{}},{"unified":"1F69C","sheet_x":36,"sheet_y":23,"skin_variations":{}},{"unified":"1F69D","sheet_x":36,"sheet_y":24,"skin_variations":{}},{"unified":"1F69E","sheet_x":36,"sheet_y":25,"skin_variations":{}},{"unified":"1F69F","sheet_x":36,"sheet_y":26,"skin_variations":{}},{"unified":"1F6A0","sheet_x":36,"sheet_y":27,"skin_variations":{}},{"unified":"1F6A1","sheet_x":36,"sheet_y":28,"skin_variations":{}},{"unified":"1F6A2","sheet_x":36,"sheet_y":29,"skin_variations":{}},{"unified":"1F6A3-200D-2640-FE0F","sheet_x":36,"sheet_y":30,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB-200D-2640-FE0F","sheet_x":36,"sheet_y":31},"1F3FC":{"unified":"1F6A3-1F3FC-200D-2640-FE0F","sheet_x":36,"sheet_y":32},"1F3FD":{"unified":"1F6A3-1F3FD-200D-2640-FE0F","sheet_x":36,"sheet_y":33},"1F3FE":{"unified":"1F6A3-1F3FE-200D-2640-FE0F","sheet_x":36,"sheet_y":34},"1F3FF":{"unified":"1F6A3-1F3FF-200D-2640-FE0F","sheet_x":36,"sheet_y":35}}},{"unified":"1F6A3-200D-2642-FE0F","sheet_x":36,"sheet_y":36,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB-200D-2642-FE0F","sheet_x":36,"sheet_y":37},"1F3FC":{"unified":"1F6A3-1F3FC-200D-2642-FE0F","sheet_x":36,"sheet_y":38},"1F3FD":{"unified":"1F6A3-1F3FD-200D-2642-FE0F","sheet_x":36,"sheet_y":39},"1F3FE":{"unified":"1F6A3-1F3FE-200D-2642-FE0F","sheet_x":36,"sheet_y":40},"1F3FF":{"unified":"1F6A3-1F3FF-200D-2642-FE0F","sheet_x":36,"sheet_y":41}}},{"unified":"1F6A3","sheet_x":36,"sheet_y":42,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB","sheet_x":36,"sheet_y":43},"1F3FC":{"unified":"1F6A3-1F3FC","sheet_x":36,"sheet_y":44},"1F3FD":{"unified":"1F6A3-1F3FD","sheet_x":36,"sheet_y":45},"1F3FE":{"unified":"1F6A3-1F3FE","sheet_x":36,"sheet_y":46},"1F3FF":{"unified":"1F6A3-1F3FF","sheet_x":36,"sheet_y":47}}},{"unified":"1F6A4","sheet_x":36,"sheet_y":48,"skin_variations":{}},{"unified":"1F6A5","sheet_x":36,"sheet_y":49,"skin_variations":{}},{"unified":"1F6A6","sheet_x":36,"sheet_y":50,"skin_variations":{}},{"unified":"1F6A7","sheet_x":36,"sheet_y":51,"skin_variations":{}},{"unified":"1F6A8","sheet_x":36,"sheet_y":52,"skin_variations":{}},{"unified":"1F6A9","sheet_x":36,"sheet_y":53,"skin_variations":{}},{"unified":"1F6AA","sheet_x":36,"sheet_y":54,"skin_variations":{}},{"unified":"1F6AB","sheet_x":36,"sheet_y":55,"skin_variations":{}},{"unified":"1F6AC","sheet_x":36,"sheet_y":56,"skin_variations":{}},{"unified":"1F6AD","sheet_x":36,"sheet_y":57,"skin_variations":{}},{"unified":"1F6AE","sheet_x":36,"sheet_y":58,"skin_variations":{}},{"unified":"1F6AF","sheet_x":36,"sheet_y":59,"skin_variations":{}},{"unified":"1F6B0","sheet_x":36,"sheet_y":60,"skin_variations":{}},{"unified":"1F6B1","sheet_x":36,"sheet_y":61,"skin_variations":{}},{"unified":"1F6B2","sheet_x":37,"sheet_y":0,"skin_variations":{}},{"unified":"1F6B3","sheet_x":37,"sheet_y":1,"skin_variations":{}},{"unified":"1F6B4-200D-2640-FE0F","sheet_x":37,"sheet_y":2,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB-200D-2640-FE0F","sheet_x":37,"sheet_y":3},"1F3FC":{"unified":"1F6B4-1F3FC-200D-2640-FE0F","sheet_x":37,"sheet_y":4},"1F3FD":{"unified":"1F6B4-1F3FD-200D-2640-FE0F","sheet_x":37,"sheet_y":5},"1F3FE":{"unified":"1F6B4-1F3FE-200D-2640-FE0F","sheet_x":37,"sheet_y":6},"1F3FF":{"unified":"1F6B4-1F3FF-200D-2640-FE0F","sheet_x":37,"sheet_y":7}}},{"unified":"1F6B4-200D-2642-FE0F","sheet_x":37,"sheet_y":8,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB-200D-2642-FE0F","sheet_x":37,"sheet_y":9},"1F3FC":{"unified":"1F6B4-1F3FC-200D-2642-FE0F","sheet_x":37,"sheet_y":10},"1F3FD":{"unified":"1F6B4-1F3FD-200D-2642-FE0F","sheet_x":37,"sheet_y":11},"1F3FE":{"unified":"1F6B4-1F3FE-200D-2642-FE0F","sheet_x":37,"sheet_y":12},"1F3FF":{"unified":"1F6B4-1F3FF-200D-2642-FE0F","sheet_x":37,"sheet_y":13}}},{"unified":"1F6B4","sheet_x":37,"sheet_y":14,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB","sheet_x":37,"sheet_y":15},"1F3FC":{"unified":"1F6B4-1F3FC","sheet_x":37,"sheet_y":16},"1F3FD":{"unified":"1F6B4-1F3FD","sheet_x":37,"sheet_y":17},"1F3FE":{"unified":"1F6B4-1F3FE","sheet_x":37,"sheet_y":18},"1F3FF":{"unified":"1F6B4-1F3FF","sheet_x":37,"sheet_y":19}}},{"unified":"1F6B5-200D-2640-FE0F","sheet_x":37,"sheet_y":20,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB-200D-2640-FE0F","sheet_x":37,"sheet_y":21},"1F3FC":{"unified":"1F6B5-1F3FC-200D-2640-FE0F","sheet_x":37,"sheet_y":22},"1F3FD":{"unified":"1F6B5-1F3FD-200D-2640-FE0F","sheet_x":37,"sheet_y":23},"1F3FE":{"unified":"1F6B5-1F3FE-200D-2640-FE0F","sheet_x":37,"sheet_y":24},"1F3FF":{"unified":"1F6B5-1F3FF-200D-2640-FE0F","sheet_x":37,"sheet_y":25}}},{"unified":"1F6B5-200D-2642-FE0F","sheet_x":37,"sheet_y":26,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB-200D-2642-FE0F","sheet_x":37,"sheet_y":27},"1F3FC":{"unified":"1F6B5-1F3FC-200D-2642-FE0F","sheet_x":37,"sheet_y":28},"1F3FD":{"unified":"1F6B5-1F3FD-200D-2642-FE0F","sheet_x":37,"sheet_y":29},"1F3FE":{"unified":"1F6B5-1F3FE-200D-2642-FE0F","sheet_x":37,"sheet_y":30},"1F3FF":{"unified":"1F6B5-1F3FF-200D-2642-FE0F","sheet_x":37,"sheet_y":31}}},{"unified":"1F6B5","sheet_x":37,"sheet_y":32,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB","sheet_x":37,"sheet_y":33},"1F3FC":{"unified":"1F6B5-1F3FC","sheet_x":37,"sheet_y":34},"1F3FD":{"unified":"1F6B5-1F3FD","sheet_x":37,"sheet_y":35},"1F3FE":{"unified":"1F6B5-1F3FE","sheet_x":37,"sheet_y":36},"1F3FF":{"unified":"1F6B5-1F3FF","sheet_x":37,"sheet_y":37}}},{"unified":"1F6B6-200D-2640-FE0F","sheet_x":37,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2640-FE0F","sheet_x":37,"sheet_y":39},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2640-FE0F","sheet_x":37,"sheet_y":40},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2640-FE0F","sheet_x":37,"sheet_y":41},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2640-FE0F","sheet_x":37,"sheet_y":42},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2640-FE0F","sheet_x":37,"sheet_y":43}}},{"unified":"1F6B6-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":44,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":45},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":46},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":47},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":48},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":49}}},{"unified":"1F6B6-200D-2642-FE0F","sheet_x":37,"sheet_y":50,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2642-FE0F","sheet_x":37,"sheet_y":51},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2642-FE0F","sheet_x":37,"sheet_y":52},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2642-FE0F","sheet_x":37,"sheet_y":53},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2642-FE0F","sheet_x":37,"sheet_y":54},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2642-FE0F","sheet_x":37,"sheet_y":55}}},{"unified":"1F6B6-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":57},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":58},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":59},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":60},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":37,"sheet_y":61}}},{"unified":"1F6B6-200D-27A1-FE0F","sheet_x":38,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-27A1-FE0F","sheet_x":38,"sheet_y":1},"1F3FC":{"unified":"1F6B6-1F3FC-200D-27A1-FE0F","sheet_x":38,"sheet_y":2},"1F3FD":{"unified":"1F6B6-1F3FD-200D-27A1-FE0F","sheet_x":38,"sheet_y":3},"1F3FE":{"unified":"1F6B6-1F3FE-200D-27A1-FE0F","sheet_x":38,"sheet_y":4},"1F3FF":{"unified":"1F6B6-1F3FF-200D-27A1-FE0F","sheet_x":38,"sheet_y":5}}},{"unified":"1F6B6","sheet_x":38,"sheet_y":6,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB","sheet_x":38,"sheet_y":7},"1F3FC":{"unified":"1F6B6-1F3FC","sheet_x":38,"sheet_y":8},"1F3FD":{"unified":"1F6B6-1F3FD","sheet_x":38,"sheet_y":9},"1F3FE":{"unified":"1F6B6-1F3FE","sheet_x":38,"sheet_y":10},"1F3FF":{"unified":"1F6B6-1F3FF","sheet_x":38,"sheet_y":11}}},{"unified":"1F6B7","sheet_x":38,"sheet_y":12,"skin_variations":{}},{"unified":"1F6B8","sheet_x":38,"sheet_y":13,"skin_variations":{}},{"unified":"1F6B9","sheet_x":38,"sheet_y":14,"skin_variations":{}},{"unified":"1F6BA","sheet_x":38,"sheet_y":15,"skin_variations":{}},{"unified":"1F6BB","sheet_x":38,"sheet_y":16,"skin_variations":{}},{"unified":"1F6BC","sheet_x":38,"sheet_y":17,"skin_variations":{}},{"unified":"1F6BD","sheet_x":38,"sheet_y":18,"skin_variations":{}},{"unified":"1F6BE","sheet_x":38,"sheet_y":19,"skin_variations":{}},{"unified":"1F6BF","sheet_x":38,"sheet_y":20,"skin_variations":{}},{"unified":"1F6C0","sheet_x":38,"sheet_y":21,"skin_variations":{"1F3FB":{"unified":"1F6C0-1F3FB","sheet_x":38,"sheet_y":22},"1F3FC":{"unified":"1F6C0-1F3FC","sheet_x":38,"sheet_y":23},"1F3FD":{"unified":"1F6C0-1F3FD","sheet_x":38,"sheet_y":24},"1F3FE":{"unified":"1F6C0-1F3FE","sheet_x":38,"sheet_y":25},"1F3FF":{"unified":"1F6C0-1F3FF","sheet_x":38,"sheet_y":26}}},{"unified":"1F6C1","sheet_x":38,"sheet_y":27,"skin_variations":{}},{"unified":"1F6C2","sheet_x":38,"sheet_y":28,"skin_variations":{}},{"unified":"1F6C3","sheet_x":38,"sheet_y":29,"skin_variations":{}},{"unified":"1F6C4","sheet_x":38,"sheet_y":30,"skin_variations":{}},{"unified":"1F6C5","sheet_x":38,"sheet_y":31,"skin_variations":{}},{"unified":"1F6CB-FE0F","sheet_x":38,"sheet_y":32,"skin_variations":{}},{"unified":"1F6CC","sheet_x":38,"sheet_y":33,"skin_variations":{"1F3FB":{"unified":"1F6CC-1F3FB","sheet_x":38,"sheet_y":34},"1F3FC":{"unified":"1F6CC-1F3FC","sheet_x":38,"sheet_y":35},"1F3FD":{"unified":"1F6CC-1F3FD","sheet_x":38,"sheet_y":36},"1F3FE":{"unified":"1F6CC-1F3FE","sheet_x":38,"sheet_y":37},"1F3FF":{"unified":"1F6CC-1F3FF","sheet_x":38,"sheet_y":38}}},{"unified":"1F6CD-FE0F","sheet_x":38,"sheet_y":39,"skin_variations":{}},{"unified":"1F6CE-FE0F","sheet_x":38,"sheet_y":40,"skin_variations":{}},{"unified":"1F6CF-FE0F","sheet_x":38,"sheet_y":41,"skin_variations":{}},{"unified":"1F6D0","sheet_x":38,"sheet_y":42,"skin_variations":{}},{"unified":"1F6D1","sheet_x":38,"sheet_y":43,"skin_variations":{}},{"unified":"1F6D2","sheet_x":38,"sheet_y":44,"skin_variations":{}},{"unified":"1F6D5","sheet_x":38,"sheet_y":45,"skin_variations":{}},{"unified":"1F6D6","sheet_x":38,"sheet_y":46,"skin_variations":{}},{"unified":"1F6D7","sheet_x":38,"sheet_y":47,"skin_variations":{}},{"unified":"1F6DC","sheet_x":38,"sheet_y":48,"skin_variations":{}},{"unified":"1F6DD","sheet_x":38,"sheet_y":49,"skin_variations":{}},{"unified":"1F6DE","sheet_x":38,"sheet_y":50,"skin_variations":{}},{"unified":"1F6DF","sheet_x":38,"sheet_y":51,"skin_variations":{}},{"unified":"1F6E0-FE0F","sheet_x":38,"sheet_y":52,"skin_variations":{}},{"unified":"1F6E1-FE0F","sheet_x":38,"sheet_y":53,"skin_variations":{}},{"unified":"1F6E2-FE0F","sheet_x":38,"sheet_y":54,"skin_variations":{}},{"unified":"1F6E3-FE0F","sheet_x":38,"sheet_y":55,"skin_variations":{}},{"unified":"1F6E4-FE0F","sheet_x":38,"sheet_y":56,"skin_variations":{}},{"unified":"1F6E5-FE0F","sheet_x":38,"sheet_y":57,"skin_variations":{}},{"unified":"1F6E9-FE0F","sheet_x":38,"sheet_y":58,"skin_variations":{}},{"unified":"1F6EB","sheet_x":38,"sheet_y":59,"skin_variations":{}},{"unified":"1F6EC","sheet_x":38,"sheet_y":60,"skin_variations":{}},{"unified":"1F6F0-FE0F","sheet_x":38,"sheet_y":61,"skin_variations":{}},{"unified":"1F6F3-FE0F","sheet_x":39,"sheet_y":0,"skin_variations":{}},{"unified":"1F6F4","sheet_x":39,"sheet_y":1,"skin_variations":{}},{"unified":"1F6F5","sheet_x":39,"sheet_y":2,"skin_variations":{}},{"unified":"1F6F6","sheet_x":39,"sheet_y":3,"skin_variations":{}},{"unified":"1F6F7","sheet_x":39,"sheet_y":4,"skin_variations":{}},{"unified":"1F6F8","sheet_x":39,"sheet_y":5,"skin_variations":{}},{"unified":"1F6F9","sheet_x":39,"sheet_y":6,"skin_variations":{}},{"unified":"1F6FA","sheet_x":39,"sheet_y":7,"skin_variations":{}},{"unified":"1F6FB","sheet_x":39,"sheet_y":8,"skin_variations":{}},{"unified":"1F6FC","sheet_x":39,"sheet_y":9,"skin_variations":{}},{"unified":"1F7E0","sheet_x":39,"sheet_y":10,"skin_variations":{}},{"unified":"1F7E1","sheet_x":39,"sheet_y":11,"skin_variations":{}},{"unified":"1F7E2","sheet_x":39,"sheet_y":12,"skin_variations":{}},{"unified":"1F7E3","sheet_x":39,"sheet_y":13,"skin_variations":{}},{"unified":"1F7E4","sheet_x":39,"sheet_y":14,"skin_variations":{}},{"unified":"1F7E5","sheet_x":39,"sheet_y":15,"skin_variations":{}},{"unified":"1F7E6","sheet_x":39,"sheet_y":16,"skin_variations":{}},{"unified":"1F7E7","sheet_x":39,"sheet_y":17,"skin_variations":{}},{"unified":"1F7E8","sheet_x":39,"sheet_y":18,"skin_variations":{}},{"unified":"1F7E9","sheet_x":39,"sheet_y":19,"skin_variations":{}},{"unified":"1F7EA","sheet_x":39,"sheet_y":20,"skin_variations":{}},{"unified":"1F7EB","sheet_x":39,"sheet_y":21,"skin_variations":{}},{"unified":"1F7F0","sheet_x":39,"sheet_y":22,"skin_variations":{}},{"unified":"1F90C","sheet_x":39,"sheet_y":23,"skin_variations":{"1F3FB":{"unified":"1F90C-1F3FB","sheet_x":39,"sheet_y":24},"1F3FC":{"unified":"1F90C-1F3FC","sheet_x":39,"sheet_y":25},"1F3FD":{"unified":"1F90C-1F3FD","sheet_x":39,"sheet_y":26},"1F3FE":{"unified":"1F90C-1F3FE","sheet_x":39,"sheet_y":27},"1F3FF":{"unified":"1F90C-1F3FF","sheet_x":39,"sheet_y":28}}},{"unified":"1F90D","sheet_x":39,"sheet_y":29,"skin_variations":{}},{"unified":"1F90E","sheet_x":39,"sheet_y":30,"skin_variations":{}},{"unified":"1F90F","sheet_x":39,"sheet_y":31,"skin_variations":{"1F3FB":{"unified":"1F90F-1F3FB","sheet_x":39,"sheet_y":32},"1F3FC":{"unified":"1F90F-1F3FC","sheet_x":39,"sheet_y":33},"1F3FD":{"unified":"1F90F-1F3FD","sheet_x":39,"sheet_y":34},"1F3FE":{"unified":"1F90F-1F3FE","sheet_x":39,"sheet_y":35},"1F3FF":{"unified":"1F90F-1F3FF","sheet_x":39,"sheet_y":36}}},{"unified":"1F910","sheet_x":39,"sheet_y":37,"skin_variations":{}},{"unified":"1F911","sheet_x":39,"sheet_y":38,"skin_variations":{}},{"unified":"1F912","sheet_x":39,"sheet_y":39,"skin_variations":{}},{"unified":"1F913","sheet_x":39,"sheet_y":40,"skin_variations":{}},{"unified":"1F914","sheet_x":39,"sheet_y":41,"skin_variations":{}},{"unified":"1F915","sheet_x":39,"sheet_y":42,"skin_variations":{}},{"unified":"1F916","sheet_x":39,"sheet_y":43,"skin_variations":{}},{"unified":"1F917","sheet_x":39,"sheet_y":44,"skin_variations":{}},{"unified":"1F918","sheet_x":39,"sheet_y":45,"skin_variations":{"1F3FB":{"unified":"1F918-1F3FB","sheet_x":39,"sheet_y":46},"1F3FC":{"unified":"1F918-1F3FC","sheet_x":39,"sheet_y":47},"1F3FD":{"unified":"1F918-1F3FD","sheet_x":39,"sheet_y":48},"1F3FE":{"unified":"1F918-1F3FE","sheet_x":39,"sheet_y":49},"1F3FF":{"unified":"1F918-1F3FF","sheet_x":39,"sheet_y":50}}},{"unified":"1F919","sheet_x":39,"sheet_y":51,"skin_variations":{"1F3FB":{"unified":"1F919-1F3FB","sheet_x":39,"sheet_y":52},"1F3FC":{"unified":"1F919-1F3FC","sheet_x":39,"sheet_y":53},"1F3FD":{"unified":"1F919-1F3FD","sheet_x":39,"sheet_y":54},"1F3FE":{"unified":"1F919-1F3FE","sheet_x":39,"sheet_y":55},"1F3FF":{"unified":"1F919-1F3FF","sheet_x":39,"sheet_y":56}}},{"unified":"1F91A","sheet_x":39,"sheet_y":57,"skin_variations":{"1F3FB":{"unified":"1F91A-1F3FB","sheet_x":39,"sheet_y":58},"1F3FC":{"unified":"1F91A-1F3FC","sheet_x":39,"sheet_y":59},"1F3FD":{"unified":"1F91A-1F3FD","sheet_x":39,"sheet_y":60},"1F3FE":{"unified":"1F91A-1F3FE","sheet_x":39,"sheet_y":61},"1F3FF":{"unified":"1F91A-1F3FF","sheet_x":40,"sheet_y":0}}},{"unified":"1F91B","sheet_x":40,"sheet_y":1,"skin_variations":{"1F3FB":{"unified":"1F91B-1F3FB","sheet_x":40,"sheet_y":2},"1F3FC":{"unified":"1F91B-1F3FC","sheet_x":40,"sheet_y":3},"1F3FD":{"unified":"1F91B-1F3FD","sheet_x":40,"sheet_y":4},"1F3FE":{"unified":"1F91B-1F3FE","sheet_x":40,"sheet_y":5},"1F3FF":{"unified":"1F91B-1F3FF","sheet_x":40,"sheet_y":6}}},{"unified":"1F91C","sheet_x":40,"sheet_y":7,"skin_variations":{"1F3FB":{"unified":"1F91C-1F3FB","sheet_x":40,"sheet_y":8},"1F3FC":{"unified":"1F91C-1F3FC","sheet_x":40,"sheet_y":9},"1F3FD":{"unified":"1F91C-1F3FD","sheet_x":40,"sheet_y":10},"1F3FE":{"unified":"1F91C-1F3FE","sheet_x":40,"sheet_y":11},"1F3FF":{"unified":"1F91C-1F3FF","sheet_x":40,"sheet_y":12}}},{"unified":"1F91D","sheet_x":40,"sheet_y":13,"skin_variations":{"1F3FB":{"unified":"1F91D-1F3FB","sheet_x":40,"sheet_y":14},"1F3FC":{"unified":"1F91D-1F3FC","sheet_x":40,"sheet_y":15},"1F3FD":{"unified":"1F91D-1F3FD","sheet_x":40,"sheet_y":16},"1F3FE":{"unified":"1F91D-1F3FE","sheet_x":40,"sheet_y":17},"1F3FF":{"unified":"1F91D-1F3FF","sheet_x":40,"sheet_y":18},"1F3FB-1F3FC":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FC","sheet_x":40,"sheet_y":19},"1F3FB-1F3FD":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FD","sheet_x":40,"sheet_y":20},"1F3FB-1F3FE":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FE","sheet_x":40,"sheet_y":21},"1F3FB-1F3FF":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FF","sheet_x":40,"sheet_y":22},"1F3FC-1F3FB":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FB","sheet_x":40,"sheet_y":23},"1F3FC-1F3FD":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FD","sheet_x":40,"sheet_y":24},"1F3FC-1F3FE":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FE","sheet_x":40,"sheet_y":25},"1F3FC-1F3FF":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FF","sheet_x":40,"sheet_y":26},"1F3FD-1F3FB":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FB","sheet_x":40,"sheet_y":27},"1F3FD-1F3FC":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FC","sheet_x":40,"sheet_y":28},"1F3FD-1F3FE":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FE","sheet_x":40,"sheet_y":29},"1F3FD-1F3FF":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FF","sheet_x":40,"sheet_y":30},"1F3FE-1F3FB":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FB","sheet_x":40,"sheet_y":31},"1F3FE-1F3FC":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FC","sheet_x":40,"sheet_y":32},"1F3FE-1F3FD":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FD","sheet_x":40,"sheet_y":33},"1F3FE-1F3FF":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FF","sheet_x":40,"sheet_y":34},"1F3FF-1F3FB":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FB","sheet_x":40,"sheet_y":35},"1F3FF-1F3FC":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FC","sheet_x":40,"sheet_y":36},"1F3FF-1F3FD":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FD","sheet_x":40,"sheet_y":37},"1F3FF-1F3FE":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FE","sheet_x":40,"sheet_y":38}}},{"unified":"1F91E","sheet_x":40,"sheet_y":39,"skin_variations":{"1F3FB":{"unified":"1F91E-1F3FB","sheet_x":40,"sheet_y":40},"1F3FC":{"unified":"1F91E-1F3FC","sheet_x":40,"sheet_y":41},"1F3FD":{"unified":"1F91E-1F3FD","sheet_x":40,"sheet_y":42},"1F3FE":{"unified":"1F91E-1F3FE","sheet_x":40,"sheet_y":43},"1F3FF":{"unified":"1F91E-1F3FF","sheet_x":40,"sheet_y":44}}},{"unified":"1F91F","sheet_x":40,"sheet_y":45,"skin_variations":{"1F3FB":{"unified":"1F91F-1F3FB","sheet_x":40,"sheet_y":46},"1F3FC":{"unified":"1F91F-1F3FC","sheet_x":40,"sheet_y":47},"1F3FD":{"unified":"1F91F-1F3FD","sheet_x":40,"sheet_y":48},"1F3FE":{"unified":"1F91F-1F3FE","sheet_x":40,"sheet_y":49},"1F3FF":{"unified":"1F91F-1F3FF","sheet_x":40,"sheet_y":50}}},{"unified":"1F920","sheet_x":40,"sheet_y":51,"skin_variations":{}},{"unified":"1F921","sheet_x":40,"sheet_y":52,"skin_variations":{}},{"unified":"1F922","sheet_x":40,"sheet_y":53,"skin_variations":{}},{"unified":"1F923","sheet_x":40,"sheet_y":54,"skin_variations":{}},{"unified":"1F924","sheet_x":40,"sheet_y":55,"skin_variations":{}},{"unified":"1F925","sheet_x":40,"sheet_y":56,"skin_variations":{}},{"unified":"1F926-200D-2640-FE0F","sheet_x":40,"sheet_y":57,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB-200D-2640-FE0F","sheet_x":40,"sheet_y":58},"1F3FC":{"unified":"1F926-1F3FC-200D-2640-FE0F","sheet_x":40,"sheet_y":59},"1F3FD":{"unified":"1F926-1F3FD-200D-2640-FE0F","sheet_x":40,"sheet_y":60},"1F3FE":{"unified":"1F926-1F3FE-200D-2640-FE0F","sheet_x":40,"sheet_y":61},"1F3FF":{"unified":"1F926-1F3FF-200D-2640-FE0F","sheet_x":41,"sheet_y":0}}},{"unified":"1F926-200D-2642-FE0F","sheet_x":41,"sheet_y":1,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB-200D-2642-FE0F","sheet_x":41,"sheet_y":2},"1F3FC":{"unified":"1F926-1F3FC-200D-2642-FE0F","sheet_x":41,"sheet_y":3},"1F3FD":{"unified":"1F926-1F3FD-200D-2642-FE0F","sheet_x":41,"sheet_y":4},"1F3FE":{"unified":"1F926-1F3FE-200D-2642-FE0F","sheet_x":41,"sheet_y":5},"1F3FF":{"unified":"1F926-1F3FF-200D-2642-FE0F","sheet_x":41,"sheet_y":6}}},{"unified":"1F926","sheet_x":41,"sheet_y":7,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB","sheet_x":41,"sheet_y":8},"1F3FC":{"unified":"1F926-1F3FC","sheet_x":41,"sheet_y":9},"1F3FD":{"unified":"1F926-1F3FD","sheet_x":41,"sheet_y":10},"1F3FE":{"unified":"1F926-1F3FE","sheet_x":41,"sheet_y":11},"1F3FF":{"unified":"1F926-1F3FF","sheet_x":41,"sheet_y":12}}},{"unified":"1F927","sheet_x":41,"sheet_y":13,"skin_variations":{}},{"unified":"1F928","sheet_x":41,"sheet_y":14,"skin_variations":{}},{"unified":"1F929","sheet_x":41,"sheet_y":15,"skin_variations":{}},{"unified":"1F92A","sheet_x":41,"sheet_y":16,"skin_variations":{}},{"unified":"1F92B","sheet_x":41,"sheet_y":17,"skin_variations":{}},{"unified":"1F92C","sheet_x":41,"sheet_y":18,"skin_variations":{}},{"unified":"1F92D","sheet_x":41,"sheet_y":19,"skin_variations":{}},{"unified":"1F92E","sheet_x":41,"sheet_y":20,"skin_variations":{}},{"unified":"1F92F","sheet_x":41,"sheet_y":21,"skin_variations":{}},{"unified":"1F930","sheet_x":41,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F930-1F3FB","sheet_x":41,"sheet_y":23},"1F3FC":{"unified":"1F930-1F3FC","sheet_x":41,"sheet_y":24},"1F3FD":{"unified":"1F930-1F3FD","sheet_x":41,"sheet_y":25},"1F3FE":{"unified":"1F930-1F3FE","sheet_x":41,"sheet_y":26},"1F3FF":{"unified":"1F930-1F3FF","sheet_x":41,"sheet_y":27}}},{"unified":"1F931","sheet_x":41,"sheet_y":28,"skin_variations":{"1F3FB":{"unified":"1F931-1F3FB","sheet_x":41,"sheet_y":29},"1F3FC":{"unified":"1F931-1F3FC","sheet_x":41,"sheet_y":30},"1F3FD":{"unified":"1F931-1F3FD","sheet_x":41,"sheet_y":31},"1F3FE":{"unified":"1F931-1F3FE","sheet_x":41,"sheet_y":32},"1F3FF":{"unified":"1F931-1F3FF","sheet_x":41,"sheet_y":33}}},{"unified":"1F932","sheet_x":41,"sheet_y":34,"skin_variations":{"1F3FB":{"unified":"1F932-1F3FB","sheet_x":41,"sheet_y":35},"1F3FC":{"unified":"1F932-1F3FC","sheet_x":41,"sheet_y":36},"1F3FD":{"unified":"1F932-1F3FD","sheet_x":41,"sheet_y":37},"1F3FE":{"unified":"1F932-1F3FE","sheet_x":41,"sheet_y":38},"1F3FF":{"unified":"1F932-1F3FF","sheet_x":41,"sheet_y":39}}},{"unified":"1F933","sheet_x":41,"sheet_y":40,"skin_variations":{"1F3FB":{"unified":"1F933-1F3FB","sheet_x":41,"sheet_y":41},"1F3FC":{"unified":"1F933-1F3FC","sheet_x":41,"sheet_y":42},"1F3FD":{"unified":"1F933-1F3FD","sheet_x":41,"sheet_y":43},"1F3FE":{"unified":"1F933-1F3FE","sheet_x":41,"sheet_y":44},"1F3FF":{"unified":"1F933-1F3FF","sheet_x":41,"sheet_y":45}}},{"unified":"1F934","sheet_x":41,"sheet_y":46,"skin_variations":{"1F3FB":{"unified":"1F934-1F3FB","sheet_x":41,"sheet_y":47},"1F3FC":{"unified":"1F934-1F3FC","sheet_x":41,"sheet_y":48},"1F3FD":{"unified":"1F934-1F3FD","sheet_x":41,"sheet_y":49},"1F3FE":{"unified":"1F934-1F3FE","sheet_x":41,"sheet_y":50},"1F3FF":{"unified":"1F934-1F3FF","sheet_x":41,"sheet_y":51}}},{"unified":"1F935-200D-2640-FE0F","sheet_x":41,"sheet_y":52,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB-200D-2640-FE0F","sheet_x":41,"sheet_y":53},"1F3FC":{"unified":"1F935-1F3FC-200D-2640-FE0F","sheet_x":41,"sheet_y":54},"1F3FD":{"unified":"1F935-1F3FD-200D-2640-FE0F","sheet_x":41,"sheet_y":55},"1F3FE":{"unified":"1F935-1F3FE-200D-2640-FE0F","sheet_x":41,"sheet_y":56},"1F3FF":{"unified":"1F935-1F3FF-200D-2640-FE0F","sheet_x":41,"sheet_y":57}}},{"unified":"1F935-200D-2642-FE0F","sheet_x":41,"sheet_y":58,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB-200D-2642-FE0F","sheet_x":41,"sheet_y":59},"1F3FC":{"unified":"1F935-1F3FC-200D-2642-FE0F","sheet_x":41,"sheet_y":60},"1F3FD":{"unified":"1F935-1F3FD-200D-2642-FE0F","sheet_x":41,"sheet_y":61},"1F3FE":{"unified":"1F935-1F3FE-200D-2642-FE0F","sheet_x":42,"sheet_y":0},"1F3FF":{"unified":"1F935-1F3FF-200D-2642-FE0F","sheet_x":42,"sheet_y":1}}},{"unified":"1F935","sheet_x":42,"sheet_y":2,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB","sheet_x":42,"sheet_y":3},"1F3FC":{"unified":"1F935-1F3FC","sheet_x":42,"sheet_y":4},"1F3FD":{"unified":"1F935-1F3FD","sheet_x":42,"sheet_y":5},"1F3FE":{"unified":"1F935-1F3FE","sheet_x":42,"sheet_y":6},"1F3FF":{"unified":"1F935-1F3FF","sheet_x":42,"sheet_y":7}}},{"unified":"1F936","sheet_x":42,"sheet_y":8,"skin_variations":{"1F3FB":{"unified":"1F936-1F3FB","sheet_x":42,"sheet_y":9},"1F3FC":{"unified":"1F936-1F3FC","sheet_x":42,"sheet_y":10},"1F3FD":{"unified":"1F936-1F3FD","sheet_x":42,"sheet_y":11},"1F3FE":{"unified":"1F936-1F3FE","sheet_x":42,"sheet_y":12},"1F3FF":{"unified":"1F936-1F3FF","sheet_x":42,"sheet_y":13}}},{"unified":"1F937-200D-2640-FE0F","sheet_x":42,"sheet_y":14,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB-200D-2640-FE0F","sheet_x":42,"sheet_y":15},"1F3FC":{"unified":"1F937-1F3FC-200D-2640-FE0F","sheet_x":42,"sheet_y":16},"1F3FD":{"unified":"1F937-1F3FD-200D-2640-FE0F","sheet_x":42,"sheet_y":17},"1F3FE":{"unified":"1F937-1F3FE-200D-2640-FE0F","sheet_x":42,"sheet_y":18},"1F3FF":{"unified":"1F937-1F3FF-200D-2640-FE0F","sheet_x":42,"sheet_y":19}}},{"unified":"1F937-200D-2642-FE0F","sheet_x":42,"sheet_y":20,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB-200D-2642-FE0F","sheet_x":42,"sheet_y":21},"1F3FC":{"unified":"1F937-1F3FC-200D-2642-FE0F","sheet_x":42,"sheet_y":22},"1F3FD":{"unified":"1F937-1F3FD-200D-2642-FE0F","sheet_x":42,"sheet_y":23},"1F3FE":{"unified":"1F937-1F3FE-200D-2642-FE0F","sheet_x":42,"sheet_y":24},"1F3FF":{"unified":"1F937-1F3FF-200D-2642-FE0F","sheet_x":42,"sheet_y":25}}},{"unified":"1F937","sheet_x":42,"sheet_y":26,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB","sheet_x":42,"sheet_y":27},"1F3FC":{"unified":"1F937-1F3FC","sheet_x":42,"sheet_y":28},"1F3FD":{"unified":"1F937-1F3FD","sheet_x":42,"sheet_y":29},"1F3FE":{"unified":"1F937-1F3FE","sheet_x":42,"sheet_y":30},"1F3FF":{"unified":"1F937-1F3FF","sheet_x":42,"sheet_y":31}}},{"unified":"1F938-200D-2640-FE0F","sheet_x":42,"sheet_y":32,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB-200D-2640-FE0F","sheet_x":42,"sheet_y":33},"1F3FC":{"unified":"1F938-1F3FC-200D-2640-FE0F","sheet_x":42,"sheet_y":34},"1F3FD":{"unified":"1F938-1F3FD-200D-2640-FE0F","sheet_x":42,"sheet_y":35},"1F3FE":{"unified":"1F938-1F3FE-200D-2640-FE0F","sheet_x":42,"sheet_y":36},"1F3FF":{"unified":"1F938-1F3FF-200D-2640-FE0F","sheet_x":42,"sheet_y":37}}},{"unified":"1F938-200D-2642-FE0F","sheet_x":42,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB-200D-2642-FE0F","sheet_x":42,"sheet_y":39},"1F3FC":{"unified":"1F938-1F3FC-200D-2642-FE0F","sheet_x":42,"sheet_y":40},"1F3FD":{"unified":"1F938-1F3FD-200D-2642-FE0F","sheet_x":42,"sheet_y":41},"1F3FE":{"unified":"1F938-1F3FE-200D-2642-FE0F","sheet_x":42,"sheet_y":42},"1F3FF":{"unified":"1F938-1F3FF-200D-2642-FE0F","sheet_x":42,"sheet_y":43}}},{"unified":"1F938","sheet_x":42,"sheet_y":44,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB","sheet_x":42,"sheet_y":45},"1F3FC":{"unified":"1F938-1F3FC","sheet_x":42,"sheet_y":46},"1F3FD":{"unified":"1F938-1F3FD","sheet_x":42,"sheet_y":47},"1F3FE":{"unified":"1F938-1F3FE","sheet_x":42,"sheet_y":48},"1F3FF":{"unified":"1F938-1F3FF","sheet_x":42,"sheet_y":49}}},{"unified":"1F939-200D-2640-FE0F","sheet_x":42,"sheet_y":50,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB-200D-2640-FE0F","sheet_x":42,"sheet_y":51},"1F3FC":{"unified":"1F939-1F3FC-200D-2640-FE0F","sheet_x":42,"sheet_y":52},"1F3FD":{"unified":"1F939-1F3FD-200D-2640-FE0F","sheet_x":42,"sheet_y":53},"1F3FE":{"unified":"1F939-1F3FE-200D-2640-FE0F","sheet_x":42,"sheet_y":54},"1F3FF":{"unified":"1F939-1F3FF-200D-2640-FE0F","sheet_x":42,"sheet_y":55}}},{"unified":"1F939-200D-2642-FE0F","sheet_x":42,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB-200D-2642-FE0F","sheet_x":42,"sheet_y":57},"1F3FC":{"unified":"1F939-1F3FC-200D-2642-FE0F","sheet_x":42,"sheet_y":58},"1F3FD":{"unified":"1F939-1F3FD-200D-2642-FE0F","sheet_x":42,"sheet_y":59},"1F3FE":{"unified":"1F939-1F3FE-200D-2642-FE0F","sheet_x":42,"sheet_y":60},"1F3FF":{"unified":"1F939-1F3FF-200D-2642-FE0F","sheet_x":42,"sheet_y":61}}},{"unified":"1F939","sheet_x":43,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB","sheet_x":43,"sheet_y":1},"1F3FC":{"unified":"1F939-1F3FC","sheet_x":43,"sheet_y":2},"1F3FD":{"unified":"1F939-1F3FD","sheet_x":43,"sheet_y":3},"1F3FE":{"unified":"1F939-1F3FE","sheet_x":43,"sheet_y":4},"1F3FF":{"unified":"1F939-1F3FF","sheet_x":43,"sheet_y":5}}},{"unified":"1F93A","sheet_x":43,"sheet_y":6,"skin_variations":{}},{"unified":"1F93C-200D-2640-FE0F","sheet_x":43,"sheet_y":7,"skin_variations":{}},{"unified":"1F93C-200D-2642-FE0F","sheet_x":43,"sheet_y":8,"skin_variations":{}},{"unified":"1F93C","sheet_x":43,"sheet_y":9,"skin_variations":{}},{"unified":"1F93D-200D-2640-FE0F","sheet_x":43,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB-200D-2640-FE0F","sheet_x":43,"sheet_y":11},"1F3FC":{"unified":"1F93D-1F3FC-200D-2640-FE0F","sheet_x":43,"sheet_y":12},"1F3FD":{"unified":"1F93D-1F3FD-200D-2640-FE0F","sheet_x":43,"sheet_y":13},"1F3FE":{"unified":"1F93D-1F3FE-200D-2640-FE0F","sheet_x":43,"sheet_y":14},"1F3FF":{"unified":"1F93D-1F3FF-200D-2640-FE0F","sheet_x":43,"sheet_y":15}}},{"unified":"1F93D-200D-2642-FE0F","sheet_x":43,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB-200D-2642-FE0F","sheet_x":43,"sheet_y":17},"1F3FC":{"unified":"1F93D-1F3FC-200D-2642-FE0F","sheet_x":43,"sheet_y":18},"1F3FD":{"unified":"1F93D-1F3FD-200D-2642-FE0F","sheet_x":43,"sheet_y":19},"1F3FE":{"unified":"1F93D-1F3FE-200D-2642-FE0F","sheet_x":43,"sheet_y":20},"1F3FF":{"unified":"1F93D-1F3FF-200D-2642-FE0F","sheet_x":43,"sheet_y":21}}},{"unified":"1F93D","sheet_x":43,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB","sheet_x":43,"sheet_y":23},"1F3FC":{"unified":"1F93D-1F3FC","sheet_x":43,"sheet_y":24},"1F3FD":{"unified":"1F93D-1F3FD","sheet_x":43,"sheet_y":25},"1F3FE":{"unified":"1F93D-1F3FE","sheet_x":43,"sheet_y":26},"1F3FF":{"unified":"1F93D-1F3FF","sheet_x":43,"sheet_y":27}}},{"unified":"1F93E-200D-2640-FE0F","sheet_x":43,"sheet_y":28,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB-200D-2640-FE0F","sheet_x":43,"sheet_y":29},"1F3FC":{"unified":"1F93E-1F3FC-200D-2640-FE0F","sheet_x":43,"sheet_y":30},"1F3FD":{"unified":"1F93E-1F3FD-200D-2640-FE0F","sheet_x":43,"sheet_y":31},"1F3FE":{"unified":"1F93E-1F3FE-200D-2640-FE0F","sheet_x":43,"sheet_y":32},"1F3FF":{"unified":"1F93E-1F3FF-200D-2640-FE0F","sheet_x":43,"sheet_y":33}}},{"unified":"1F93E-200D-2642-FE0F","sheet_x":43,"sheet_y":34,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB-200D-2642-FE0F","sheet_x":43,"sheet_y":35},"1F3FC":{"unified":"1F93E-1F3FC-200D-2642-FE0F","sheet_x":43,"sheet_y":36},"1F3FD":{"unified":"1F93E-1F3FD-200D-2642-FE0F","sheet_x":43,"sheet_y":37},"1F3FE":{"unified":"1F93E-1F3FE-200D-2642-FE0F","sheet_x":43,"sheet_y":38},"1F3FF":{"unified":"1F93E-1F3FF-200D-2642-FE0F","sheet_x":43,"sheet_y":39}}},{"unified":"1F93E","sheet_x":43,"sheet_y":40,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB","sheet_x":43,"sheet_y":41},"1F3FC":{"unified":"1F93E-1F3FC","sheet_x":43,"sheet_y":42},"1F3FD":{"unified":"1F93E-1F3FD","sheet_x":43,"sheet_y":43},"1F3FE":{"unified":"1F93E-1F3FE","sheet_x":43,"sheet_y":44},"1F3FF":{"unified":"1F93E-1F3FF","sheet_x":43,"sheet_y":45}}},{"unified":"1F93F","sheet_x":43,"sheet_y":46,"skin_variations":{}},{"unified":"1F940","sheet_x":43,"sheet_y":47,"skin_variations":{}},{"unified":"1F941","sheet_x":43,"sheet_y":48,"skin_variations":{}},{"unified":"1F942","sheet_x":43,"sheet_y":49,"skin_variations":{}},{"unified":"1F943","sheet_x":43,"sheet_y":50,"skin_variations":{}},{"unified":"1F944","sheet_x":43,"sheet_y":51,"skin_variations":{}},{"unified":"1F945","sheet_x":43,"sheet_y":52,"skin_variations":{}},{"unified":"1F947","sheet_x":43,"sheet_y":53,"skin_variations":{}},{"unified":"1F948","sheet_x":43,"sheet_y":54,"skin_variations":{}},{"unified":"1F949","sheet_x":43,"sheet_y":55,"skin_variations":{}},{"unified":"1F94A","sheet_x":43,"sheet_y":56,"skin_variations":{}},{"unified":"1F94B","sheet_x":43,"sheet_y":57,"skin_variations":{}},{"unified":"1F94C","sheet_x":43,"sheet_y":58,"skin_variations":{}},{"unified":"1F94D","sheet_x":43,"sheet_y":59,"skin_variations":{}},{"unified":"1F94E","sheet_x":43,"sheet_y":60,"skin_variations":{}},{"unified":"1F94F","sheet_x":43,"sheet_y":61,"skin_variations":{}},{"unified":"1F950","sheet_x":44,"sheet_y":0,"skin_variations":{}},{"unified":"1F951","sheet_x":44,"sheet_y":1,"skin_variations":{}},{"unified":"1F952","sheet_x":44,"sheet_y":2,"skin_variations":{}},{"unified":"1F953","sheet_x":44,"sheet_y":3,"skin_variations":{}},{"unified":"1F954","sheet_x":44,"sheet_y":4,"skin_variations":{}},{"unified":"1F955","sheet_x":44,"sheet_y":5,"skin_variations":{}},{"unified":"1F956","sheet_x":44,"sheet_y":6,"skin_variations":{}},{"unified":"1F957","sheet_x":44,"sheet_y":7,"skin_variations":{}},{"unified":"1F958","sheet_x":44,"sheet_y":8,"skin_variations":{}},{"unified":"1F959","sheet_x":44,"sheet_y":9,"skin_variations":{}},{"unified":"1F95A","sheet_x":44,"sheet_y":10,"skin_variations":{}},{"unified":"1F95B","sheet_x":44,"sheet_y":11,"skin_variations":{}},{"unified":"1F95C","sheet_x":44,"sheet_y":12,"skin_variations":{}},{"unified":"1F95D","sheet_x":44,"sheet_y":13,"skin_variations":{}},{"unified":"1F95E","sheet_x":44,"sheet_y":14,"skin_variations":{}},{"unified":"1F95F","sheet_x":44,"sheet_y":15,"skin_variations":{}},{"unified":"1F960","sheet_x":44,"sheet_y":16,"skin_variations":{}},{"unified":"1F961","sheet_x":44,"sheet_y":17,"skin_variations":{}},{"unified":"1F962","sheet_x":44,"sheet_y":18,"skin_variations":{}},{"unified":"1F963","sheet_x":44,"sheet_y":19,"skin_variations":{}},{"unified":"1F964","sheet_x":44,"sheet_y":20,"skin_variations":{}},{"unified":"1F965","sheet_x":44,"sheet_y":21,"skin_variations":{}},{"unified":"1F966","sheet_x":44,"sheet_y":22,"skin_variations":{}},{"unified":"1F967","sheet_x":44,"sheet_y":23,"skin_variations":{}},{"unified":"1F968","sheet_x":44,"sheet_y":24,"skin_variations":{}},{"unified":"1F969","sheet_x":44,"sheet_y":25,"skin_variations":{}},{"unified":"1F96A","sheet_x":44,"sheet_y":26,"skin_variations":{}},{"unified":"1F96B","sheet_x":44,"sheet_y":27,"skin_variations":{}},{"unified":"1F96C","sheet_x":44,"sheet_y":28,"skin_variations":{}},{"unified":"1F96D","sheet_x":44,"sheet_y":29,"skin_variations":{}},{"unified":"1F96E","sheet_x":44,"sheet_y":30,"skin_variations":{}},{"unified":"1F96F","sheet_x":44,"sheet_y":31,"skin_variations":{}},{"unified":"1F970","sheet_x":44,"sheet_y":32,"skin_variations":{}},{"unified":"1F971","sheet_x":44,"sheet_y":33,"skin_variations":{}},{"unified":"1F972","sheet_x":44,"sheet_y":34,"skin_variations":{}},{"unified":"1F973","sheet_x":44,"sheet_y":35,"skin_variations":{}},{"unified":"1F974","sheet_x":44,"sheet_y":36,"skin_variations":{}},{"unified":"1F975","sheet_x":44,"sheet_y":37,"skin_variations":{}},{"unified":"1F976","sheet_x":44,"sheet_y":38,"skin_variations":{}},{"unified":"1F977","sheet_x":44,"sheet_y":39,"skin_variations":{"1F3FB":{"unified":"1F977-1F3FB","sheet_x":44,"sheet_y":40},"1F3FC":{"unified":"1F977-1F3FC","sheet_x":44,"sheet_y":41},"1F3FD":{"unified":"1F977-1F3FD","sheet_x":44,"sheet_y":42},"1F3FE":{"unified":"1F977-1F3FE","sheet_x":44,"sheet_y":43},"1F3FF":{"unified":"1F977-1F3FF","sheet_x":44,"sheet_y":44}}},{"unified":"1F978","sheet_x":44,"sheet_y":45,"skin_variations":{}},{"unified":"1F979","sheet_x":44,"sheet_y":46,"skin_variations":{}},{"unified":"1F97A","sheet_x":44,"sheet_y":47,"skin_variations":{}},{"unified":"1F97B","sheet_x":44,"sheet_y":48,"skin_variations":{}},{"unified":"1F97C","sheet_x":44,"sheet_y":49,"skin_variations":{}},{"unified":"1F97D","sheet_x":44,"sheet_y":50,"skin_variations":{}},{"unified":"1F97E","sheet_x":44,"sheet_y":51,"skin_variations":{}},{"unified":"1F97F","sheet_x":44,"sheet_y":52,"skin_variations":{}},{"unified":"1F980","sheet_x":44,"sheet_y":53,"skin_variations":{}},{"unified":"1F981","sheet_x":44,"sheet_y":54,"skin_variations":{}},{"unified":"1F982","sheet_x":44,"sheet_y":55,"skin_variations":{}},{"unified":"1F983","sheet_x":44,"sheet_y":56,"skin_variations":{}},{"unified":"1F984","sheet_x":44,"sheet_y":57,"skin_variations":{}},{"unified":"1F985","sheet_x":44,"sheet_y":58,"skin_variations":{}},{"unified":"1F986","sheet_x":44,"sheet_y":59,"skin_variations":{}},{"unified":"1F987","sheet_x":44,"sheet_y":60,"skin_variations":{}},{"unified":"1F988","sheet_x":44,"sheet_y":61,"skin_variations":{}},{"unified":"1F989","sheet_x":45,"sheet_y":0,"skin_variations":{}},{"unified":"1F98A","sheet_x":45,"sheet_y":1,"skin_variations":{}},{"unified":"1F98B","sheet_x":45,"sheet_y":2,"skin_variations":{}},{"unified":"1F98C","sheet_x":45,"sheet_y":3,"skin_variations":{}},{"unified":"1F98D","sheet_x":45,"sheet_y":4,"skin_variations":{}},{"unified":"1F98E","sheet_x":45,"sheet_y":5,"skin_variations":{}},{"unified":"1F98F","sheet_x":45,"sheet_y":6,"skin_variations":{}},{"unified":"1F990","sheet_x":45,"sheet_y":7,"skin_variations":{}},{"unified":"1F991","sheet_x":45,"sheet_y":8,"skin_variations":{}},{"unified":"1F992","sheet_x":45,"sheet_y":9,"skin_variations":{}},{"unified":"1F993","sheet_x":45,"sheet_y":10,"skin_variations":{}},{"unified":"1F994","sheet_x":45,"sheet_y":11,"skin_variations":{}},{"unified":"1F995","sheet_x":45,"sheet_y":12,"skin_variations":{}},{"unified":"1F996","sheet_x":45,"sheet_y":13,"skin_variations":{}},{"unified":"1F997","sheet_x":45,"sheet_y":14,"skin_variations":{}},{"unified":"1F998","sheet_x":45,"sheet_y":15,"skin_variations":{}},{"unified":"1F999","sheet_x":45,"sheet_y":16,"skin_variations":{}},{"unified":"1F99A","sheet_x":45,"sheet_y":17,"skin_variations":{}},{"unified":"1F99B","sheet_x":45,"sheet_y":18,"skin_variations":{}},{"unified":"1F99C","sheet_x":45,"sheet_y":19,"skin_variations":{}},{"unified":"1F99D","sheet_x":45,"sheet_y":20,"skin_variations":{}},{"unified":"1F99E","sheet_x":45,"sheet_y":21,"skin_variations":{}},{"unified":"1F99F","sheet_x":45,"sheet_y":22,"skin_variations":{}},{"unified":"1F9A0","sheet_x":45,"sheet_y":23,"skin_variations":{}},{"unified":"1F9A1","sheet_x":45,"sheet_y":24,"skin_variations":{}},{"unified":"1F9A2","sheet_x":45,"sheet_y":25,"skin_variations":{}},{"unified":"1F9A3","sheet_x":45,"sheet_y":26,"skin_variations":{}},{"unified":"1F9A4","sheet_x":45,"sheet_y":27,"skin_variations":{}},{"unified":"1F9A5","sheet_x":45,"sheet_y":28,"skin_variations":{}},{"unified":"1F9A6","sheet_x":45,"sheet_y":29,"skin_variations":{}},{"unified":"1F9A7","sheet_x":45,"sheet_y":30,"skin_variations":{}},{"unified":"1F9A8","sheet_x":45,"sheet_y":31,"skin_variations":{}},{"unified":"1F9A9","sheet_x":45,"sheet_y":32,"skin_variations":{}},{"unified":"1F9AA","sheet_x":45,"sheet_y":33,"skin_variations":{}},{"unified":"1F9AB","sheet_x":45,"sheet_y":34,"skin_variations":{}},{"unified":"1F9AC","sheet_x":45,"sheet_y":35,"skin_variations":{}},{"unified":"1F9AD","sheet_x":45,"sheet_y":36,"skin_variations":{}},{"unified":"1F9AE","sheet_x":45,"sheet_y":37,"skin_variations":{}},{"unified":"1F9AF","sheet_x":45,"sheet_y":38,"skin_variations":{}},{"unified":"1F9B4","sheet_x":45,"sheet_y":39,"skin_variations":{}},{"unified":"1F9B5","sheet_x":45,"sheet_y":40,"skin_variations":{"1F3FB":{"unified":"1F9B5-1F3FB","sheet_x":45,"sheet_y":41},"1F3FC":{"unified":"1F9B5-1F3FC","sheet_x":45,"sheet_y":42},"1F3FD":{"unified":"1F9B5-1F3FD","sheet_x":45,"sheet_y":43},"1F3FE":{"unified":"1F9B5-1F3FE","sheet_x":45,"sheet_y":44},"1F3FF":{"unified":"1F9B5-1F3FF","sheet_x":45,"sheet_y":45}}},{"unified":"1F9B6","sheet_x":45,"sheet_y":46,"skin_variations":{"1F3FB":{"unified":"1F9B6-1F3FB","sheet_x":45,"sheet_y":47},"1F3FC":{"unified":"1F9B6-1F3FC","sheet_x":45,"sheet_y":48},"1F3FD":{"unified":"1F9B6-1F3FD","sheet_x":45,"sheet_y":49},"1F3FE":{"unified":"1F9B6-1F3FE","sheet_x":45,"sheet_y":50},"1F3FF":{"unified":"1F9B6-1F3FF","sheet_x":45,"sheet_y":51}}},{"unified":"1F9B7","sheet_x":45,"sheet_y":52,"skin_variations":{}},{"unified":"1F9B8-200D-2640-FE0F","sheet_x":45,"sheet_y":53,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB-200D-2640-FE0F","sheet_x":45,"sheet_y":54},"1F3FC":{"unified":"1F9B8-1F3FC-200D-2640-FE0F","sheet_x":45,"sheet_y":55},"1F3FD":{"unified":"1F9B8-1F3FD-200D-2640-FE0F","sheet_x":45,"sheet_y":56},"1F3FE":{"unified":"1F9B8-1F3FE-200D-2640-FE0F","sheet_x":45,"sheet_y":57},"1F3FF":{"unified":"1F9B8-1F3FF-200D-2640-FE0F","sheet_x":45,"sheet_y":58}}},{"unified":"1F9B8-200D-2642-FE0F","sheet_x":45,"sheet_y":59,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB-200D-2642-FE0F","sheet_x":45,"sheet_y":60},"1F3FC":{"unified":"1F9B8-1F3FC-200D-2642-FE0F","sheet_x":45,"sheet_y":61},"1F3FD":{"unified":"1F9B8-1F3FD-200D-2642-FE0F","sheet_x":46,"sheet_y":0},"1F3FE":{"unified":"1F9B8-1F3FE-200D-2642-FE0F","sheet_x":46,"sheet_y":1},"1F3FF":{"unified":"1F9B8-1F3FF-200D-2642-FE0F","sheet_x":46,"sheet_y":2}}},{"unified":"1F9B8","sheet_x":46,"sheet_y":3,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB","sheet_x":46,"sheet_y":4},"1F3FC":{"unified":"1F9B8-1F3FC","sheet_x":46,"sheet_y":5},"1F3FD":{"unified":"1F9B8-1F3FD","sheet_x":46,"sheet_y":6},"1F3FE":{"unified":"1F9B8-1F3FE","sheet_x":46,"sheet_y":7},"1F3FF":{"unified":"1F9B8-1F3FF","sheet_x":46,"sheet_y":8}}},{"unified":"1F9B9-200D-2640-FE0F","sheet_x":46,"sheet_y":9,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB-200D-2640-FE0F","sheet_x":46,"sheet_y":10},"1F3FC":{"unified":"1F9B9-1F3FC-200D-2640-FE0F","sheet_x":46,"sheet_y":11},"1F3FD":{"unified":"1F9B9-1F3FD-200D-2640-FE0F","sheet_x":46,"sheet_y":12},"1F3FE":{"unified":"1F9B9-1F3FE-200D-2640-FE0F","sheet_x":46,"sheet_y":13},"1F3FF":{"unified":"1F9B9-1F3FF-200D-2640-FE0F","sheet_x":46,"sheet_y":14}}},{"unified":"1F9B9-200D-2642-FE0F","sheet_x":46,"sheet_y":15,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB-200D-2642-FE0F","sheet_x":46,"sheet_y":16},"1F3FC":{"unified":"1F9B9-1F3FC-200D-2642-FE0F","sheet_x":46,"sheet_y":17},"1F3FD":{"unified":"1F9B9-1F3FD-200D-2642-FE0F","sheet_x":46,"sheet_y":18},"1F3FE":{"unified":"1F9B9-1F3FE-200D-2642-FE0F","sheet_x":46,"sheet_y":19},"1F3FF":{"unified":"1F9B9-1F3FF-200D-2642-FE0F","sheet_x":46,"sheet_y":20}}},{"unified":"1F9B9","sheet_x":46,"sheet_y":21,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB","sheet_x":46,"sheet_y":22},"1F3FC":{"unified":"1F9B9-1F3FC","sheet_x":46,"sheet_y":23},"1F3FD":{"unified":"1F9B9-1F3FD","sheet_x":46,"sheet_y":24},"1F3FE":{"unified":"1F9B9-1F3FE","sheet_x":46,"sheet_y":25},"1F3FF":{"unified":"1F9B9-1F3FF","sheet_x":46,"sheet_y":26}}},{"unified":"1F9BA","sheet_x":46,"sheet_y":27,"skin_variations":{}},{"unified":"1F9BB","sheet_x":46,"sheet_y":28,"skin_variations":{"1F3FB":{"unified":"1F9BB-1F3FB","sheet_x":46,"sheet_y":29},"1F3FC":{"unified":"1F9BB-1F3FC","sheet_x":46,"sheet_y":30},"1F3FD":{"unified":"1F9BB-1F3FD","sheet_x":46,"sheet_y":31},"1F3FE":{"unified":"1F9BB-1F3FE","sheet_x":46,"sheet_y":32},"1F3FF":{"unified":"1F9BB-1F3FF","sheet_x":46,"sheet_y":33}}},{"unified":"1F9BC","sheet_x":46,"sheet_y":34,"skin_variations":{}},{"unified":"1F9BD","sheet_x":46,"sheet_y":35,"skin_variations":{}},{"unified":"1F9BE","sheet_x":46,"sheet_y":36,"skin_variations":{}},{"unified":"1F9BF","sheet_x":46,"sheet_y":37,"skin_variations":{}},{"unified":"1F9C0","sheet_x":46,"sheet_y":38,"skin_variations":{}},{"unified":"1F9C1","sheet_x":46,"sheet_y":39,"skin_variations":{}},{"unified":"1F9C2","sheet_x":46,"sheet_y":40,"skin_variations":{}},{"unified":"1F9C3","sheet_x":46,"sheet_y":41,"skin_variations":{}},{"unified":"1F9C4","sheet_x":46,"sheet_y":42,"skin_variations":{}},{"unified":"1F9C5","sheet_x":46,"sheet_y":43,"skin_variations":{}},{"unified":"1F9C6","sheet_x":46,"sheet_y":44,"skin_variations":{}},{"unified":"1F9C7","sheet_x":46,"sheet_y":45,"skin_variations":{}},{"unified":"1F9C8","sheet_x":46,"sheet_y":46,"skin_variations":{}},{"unified":"1F9C9","sheet_x":46,"sheet_y":47,"skin_variations":{}},{"unified":"1F9CA","sheet_x":46,"sheet_y":48,"skin_variations":{}},{"unified":"1F9CB","sheet_x":46,"sheet_y":49,"skin_variations":{}},{"unified":"1F9CC","sheet_x":46,"sheet_y":50,"skin_variations":{}},{"unified":"1F9CD-200D-2640-FE0F","sheet_x":46,"sheet_y":51,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB-200D-2640-FE0F","sheet_x":46,"sheet_y":52},"1F3FC":{"unified":"1F9CD-1F3FC-200D-2640-FE0F","sheet_x":46,"sheet_y":53},"1F3FD":{"unified":"1F9CD-1F3FD-200D-2640-FE0F","sheet_x":46,"sheet_y":54},"1F3FE":{"unified":"1F9CD-1F3FE-200D-2640-FE0F","sheet_x":46,"sheet_y":55},"1F3FF":{"unified":"1F9CD-1F3FF-200D-2640-FE0F","sheet_x":46,"sheet_y":56}}},{"unified":"1F9CD-200D-2642-FE0F","sheet_x":46,"sheet_y":57,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB-200D-2642-FE0F","sheet_x":46,"sheet_y":58},"1F3FC":{"unified":"1F9CD-1F3FC-200D-2642-FE0F","sheet_x":46,"sheet_y":59},"1F3FD":{"unified":"1F9CD-1F3FD-200D-2642-FE0F","sheet_x":46,"sheet_y":60},"1F3FE":{"unified":"1F9CD-1F3FE-200D-2642-FE0F","sheet_x":46,"sheet_y":61},"1F3FF":{"unified":"1F9CD-1F3FF-200D-2642-FE0F","sheet_x":47,"sheet_y":0}}},{"unified":"1F9CD","sheet_x":47,"sheet_y":1,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB","sheet_x":47,"sheet_y":2},"1F3FC":{"unified":"1F9CD-1F3FC","sheet_x":47,"sheet_y":3},"1F3FD":{"unified":"1F9CD-1F3FD","sheet_x":47,"sheet_y":4},"1F3FE":{"unified":"1F9CD-1F3FE","sheet_x":47,"sheet_y":5},"1F3FF":{"unified":"1F9CD-1F3FF","sheet_x":47,"sheet_y":6}}},{"unified":"1F9CE-200D-2640-FE0F","sheet_x":47,"sheet_y":7,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2640-FE0F","sheet_x":47,"sheet_y":8},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2640-FE0F","sheet_x":47,"sheet_y":9},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2640-FE0F","sheet_x":47,"sheet_y":10},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2640-FE0F","sheet_x":47,"sheet_y":11},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2640-FE0F","sheet_x":47,"sheet_y":12}}},{"unified":"1F9CE-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":13,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":14},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":15},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":16},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":17},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2640-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":18}}},{"unified":"1F9CE-200D-2642-FE0F","sheet_x":47,"sheet_y":19,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2642-FE0F","sheet_x":47,"sheet_y":20},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2642-FE0F","sheet_x":47,"sheet_y":21},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2642-FE0F","sheet_x":47,"sheet_y":22},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2642-FE0F","sheet_x":47,"sheet_y":23},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2642-FE0F","sheet_x":47,"sheet_y":24}}},{"unified":"1F9CE-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":25,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":26},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":27},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":28},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":29},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2642-FE0F-200D-27A1-FE0F","sheet_x":47,"sheet_y":30}}},{"unified":"1F9CE-200D-27A1-FE0F","sheet_x":47,"sheet_y":31,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-27A1-FE0F","sheet_x":47,"sheet_y":32},"1F3FC":{"unified":"1F9CE-1F3FC-200D-27A1-FE0F","sheet_x":47,"sheet_y":33},"1F3FD":{"unified":"1F9CE-1F3FD-200D-27A1-FE0F","sheet_x":47,"sheet_y":34},"1F3FE":{"unified":"1F9CE-1F3FE-200D-27A1-FE0F","sheet_x":47,"sheet_y":35},"1F3FF":{"unified":"1F9CE-1F3FF-200D-27A1-FE0F","sheet_x":47,"sheet_y":36}}},{"unified":"1F9CE","sheet_x":47,"sheet_y":37,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB","sheet_x":47,"sheet_y":38},"1F3FC":{"unified":"1F9CE-1F3FC","sheet_x":47,"sheet_y":39},"1F3FD":{"unified":"1F9CE-1F3FD","sheet_x":47,"sheet_y":40},"1F3FE":{"unified":"1F9CE-1F3FE","sheet_x":47,"sheet_y":41},"1F3FF":{"unified":"1F9CE-1F3FF","sheet_x":47,"sheet_y":42}}},{"unified":"1F9CF-200D-2640-FE0F","sheet_x":47,"sheet_y":43,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB-200D-2640-FE0F","sheet_x":47,"sheet_y":44},"1F3FC":{"unified":"1F9CF-1F3FC-200D-2640-FE0F","sheet_x":47,"sheet_y":45},"1F3FD":{"unified":"1F9CF-1F3FD-200D-2640-FE0F","sheet_x":47,"sheet_y":46},"1F3FE":{"unified":"1F9CF-1F3FE-200D-2640-FE0F","sheet_x":47,"sheet_y":47},"1F3FF":{"unified":"1F9CF-1F3FF-200D-2640-FE0F","sheet_x":47,"sheet_y":48}}},{"unified":"1F9CF-200D-2642-FE0F","sheet_x":47,"sheet_y":49,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB-200D-2642-FE0F","sheet_x":47,"sheet_y":50},"1F3FC":{"unified":"1F9CF-1F3FC-200D-2642-FE0F","sheet_x":47,"sheet_y":51},"1F3FD":{"unified":"1F9CF-1F3FD-200D-2642-FE0F","sheet_x":47,"sheet_y":52},"1F3FE":{"unified":"1F9CF-1F3FE-200D-2642-FE0F","sheet_x":47,"sheet_y":53},"1F3FF":{"unified":"1F9CF-1F3FF-200D-2642-FE0F","sheet_x":47,"sheet_y":54}}},{"unified":"1F9CF","sheet_x":47,"sheet_y":55,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB","sheet_x":47,"sheet_y":56},"1F3FC":{"unified":"1F9CF-1F3FC","sheet_x":47,"sheet_y":57},"1F3FD":{"unified":"1F9CF-1F3FD","sheet_x":47,"sheet_y":58},"1F3FE":{"unified":"1F9CF-1F3FE","sheet_x":47,"sheet_y":59},"1F3FF":{"unified":"1F9CF-1F3FF","sheet_x":47,"sheet_y":60}}},{"unified":"1F9D0","sheet_x":47,"sheet_y":61,"skin_variations":{}},{"unified":"1F9D1-200D-1F33E","sheet_x":48,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F33E","sheet_x":48,"sheet_y":1},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F33E","sheet_x":48,"sheet_y":2},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F33E","sheet_x":48,"sheet_y":3},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F33E","sheet_x":48,"sheet_y":4},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F33E","sheet_x":48,"sheet_y":5}}},{"unified":"1F9D1-200D-1F373","sheet_x":48,"sheet_y":6,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F373","sheet_x":48,"sheet_y":7},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F373","sheet_x":48,"sheet_y":8},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F373","sheet_x":48,"sheet_y":9},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F373","sheet_x":48,"sheet_y":10},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F373","sheet_x":48,"sheet_y":11}}},{"unified":"1F9D1-200D-1F37C","sheet_x":48,"sheet_y":12,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F37C","sheet_x":48,"sheet_y":13},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F37C","sheet_x":48,"sheet_y":14},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F37C","sheet_x":48,"sheet_y":15},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F37C","sheet_x":48,"sheet_y":16},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F37C","sheet_x":48,"sheet_y":17}}},{"unified":"1F9D1-200D-1F384","sheet_x":48,"sheet_y":18,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F384","sheet_x":48,"sheet_y":19},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F384","sheet_x":48,"sheet_y":20},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F384","sheet_x":48,"sheet_y":21},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F384","sheet_x":48,"sheet_y":22},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F384","sheet_x":48,"sheet_y":23}}},{"unified":"1F9D1-200D-1F393","sheet_x":48,"sheet_y":24,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F393","sheet_x":48,"sheet_y":25},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F393","sheet_x":48,"sheet_y":26},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F393","sheet_x":48,"sheet_y":27},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F393","sheet_x":48,"sheet_y":28},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F393","sheet_x":48,"sheet_y":29}}},{"unified":"1F9D1-200D-1F3A4","sheet_x":48,"sheet_y":30,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3A4","sheet_x":48,"sheet_y":31},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3A4","sheet_x":48,"sheet_y":32},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3A4","sheet_x":48,"sheet_y":33},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3A4","sheet_x":48,"sheet_y":34},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3A4","sheet_x":48,"sheet_y":35}}},{"unified":"1F9D1-200D-1F3A8","sheet_x":48,"sheet_y":36,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3A8","sheet_x":48,"sheet_y":37},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3A8","sheet_x":48,"sheet_y":38},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3A8","sheet_x":48,"sheet_y":39},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3A8","sheet_x":48,"sheet_y":40},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3A8","sheet_x":48,"sheet_y":41}}},{"unified":"1F9D1-200D-1F3EB","sheet_x":48,"sheet_y":42,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3EB","sheet_x":48,"sheet_y":43},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3EB","sheet_x":48,"sheet_y":44},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3EB","sheet_x":48,"sheet_y":45},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3EB","sheet_x":48,"sheet_y":46},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3EB","sheet_x":48,"sheet_y":47}}},{"unified":"1F9D1-200D-1F3ED","sheet_x":48,"sheet_y":48,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3ED","sheet_x":48,"sheet_y":49},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3ED","sheet_x":48,"sheet_y":50},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3ED","sheet_x":48,"sheet_y":51},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3ED","sheet_x":48,"sheet_y":52},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3ED","sheet_x":48,"sheet_y":53}}},{"unified":"1F9D1-200D-1F4BB","sheet_x":48,"sheet_y":54,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F4BB","sheet_x":48,"sheet_y":55},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F4BB","sheet_x":48,"sheet_y":56},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F4BB","sheet_x":48,"sheet_y":57},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F4BB","sheet_x":48,"sheet_y":58},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F4BB","sheet_x":48,"sheet_y":59}}},{"unified":"1F9D1-200D-1F4BC","sheet_x":48,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F4BC","sheet_x":48,"sheet_y":61},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F4BC","sheet_x":49,"sheet_y":0},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F4BC","sheet_x":49,"sheet_y":1},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F4BC","sheet_x":49,"sheet_y":2},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F4BC","sheet_x":49,"sheet_y":3}}},{"unified":"1F9D1-200D-1F527","sheet_x":49,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F527","sheet_x":49,"sheet_y":5},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F527","sheet_x":49,"sheet_y":6},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F527","sheet_x":49,"sheet_y":7},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F527","sheet_x":49,"sheet_y":8},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F527","sheet_x":49,"sheet_y":9}}},{"unified":"1F9D1-200D-1F52C","sheet_x":49,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F52C","sheet_x":49,"sheet_y":11},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F52C","sheet_x":49,"sheet_y":12},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F52C","sheet_x":49,"sheet_y":13},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F52C","sheet_x":49,"sheet_y":14},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F52C","sheet_x":49,"sheet_y":15}}},{"unified":"1F9D1-200D-1F680","sheet_x":49,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F680","sheet_x":49,"sheet_y":17},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F680","sheet_x":49,"sheet_y":18},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F680","sheet_x":49,"sheet_y":19},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F680","sheet_x":49,"sheet_y":20},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F680","sheet_x":49,"sheet_y":21}}},{"unified":"1F9D1-200D-1F692","sheet_x":49,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F692","sheet_x":49,"sheet_y":23},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F692","sheet_x":49,"sheet_y":24},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F692","sheet_x":49,"sheet_y":25},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F692","sheet_x":49,"sheet_y":26},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F692","sheet_x":49,"sheet_y":27}}},{"unified":"1F9D1-200D-1F91D-200D-1F9D1","sheet_x":49,"sheet_y":28,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FB","sheet_x":49,"sheet_y":29},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FC","sheet_x":49,"sheet_y":30},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FD","sheet_x":49,"sheet_y":31},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FE","sheet_x":49,"sheet_y":32},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FF","sheet_x":49,"sheet_y":33},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FB","sheet_x":49,"sheet_y":34},"1F3FC-1F3FC":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FC","sheet_x":49,"sheet_y":35},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FD","sheet_x":49,"sheet_y":36},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FE","sheet_x":49,"sheet_y":37},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FF","sheet_x":49,"sheet_y":38},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FB","sheet_x":49,"sheet_y":39},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FC","sheet_x":49,"sheet_y":40},"1F3FD-1F3FD":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FD","sheet_x":49,"sheet_y":41},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FE","sheet_x":49,"sheet_y":42},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FF","sheet_x":49,"sheet_y":43},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FB","sheet_x":49,"sheet_y":44},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FC","sheet_x":49,"sheet_y":45},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FD","sheet_x":49,"sheet_y":46},"1F3FE-1F3FE":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FE","sheet_x":49,"sheet_y":47},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FF","sheet_x":49,"sheet_y":48},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FB","sheet_x":49,"sheet_y":49},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FC","sheet_x":49,"sheet_y":50},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FD","sheet_x":49,"sheet_y":51},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FE","sheet_x":49,"sheet_y":52},"1F3FF-1F3FF":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FF","sheet_x":49,"sheet_y":53}}},{"unified":"1F9D1-200D-1F9AF-200D-27A1-FE0F","sheet_x":49,"sheet_y":54,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9AF-200D-27A1-FE0F","sheet_x":49,"sheet_y":55},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9AF-200D-27A1-FE0F","sheet_x":49,"sheet_y":56},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9AF-200D-27A1-FE0F","sheet_x":49,"sheet_y":57},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9AF-200D-27A1-FE0F","sheet_x":49,"sheet_y":58},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9AF-200D-27A1-FE0F","sheet_x":49,"sheet_y":59}}},{"unified":"1F9D1-200D-1F9AF","sheet_x":49,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9AF","sheet_x":49,"sheet_y":61},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9AF","sheet_x":50,"sheet_y":0},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9AF","sheet_x":50,"sheet_y":1},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9AF","sheet_x":50,"sheet_y":2},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9AF","sheet_x":50,"sheet_y":3}}},{"unified":"1F9D1-200D-1F9B0","sheet_x":50,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B0","sheet_x":50,"sheet_y":5},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B0","sheet_x":50,"sheet_y":6},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B0","sheet_x":50,"sheet_y":7},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B0","sheet_x":50,"sheet_y":8},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B0","sheet_x":50,"sheet_y":9}}},{"unified":"1F9D1-200D-1F9B1","sheet_x":50,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B1","sheet_x":50,"sheet_y":11},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B1","sheet_x":50,"sheet_y":12},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B1","sheet_x":50,"sheet_y":13},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B1","sheet_x":50,"sheet_y":14},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B1","sheet_x":50,"sheet_y":15}}},{"unified":"1F9D1-200D-1F9B2","sheet_x":50,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B2","sheet_x":50,"sheet_y":17},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B2","sheet_x":50,"sheet_y":18},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B2","sheet_x":50,"sheet_y":19},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B2","sheet_x":50,"sheet_y":20},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B2","sheet_x":50,"sheet_y":21}}},{"unified":"1F9D1-200D-1F9B3","sheet_x":50,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B3","sheet_x":50,"sheet_y":23},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B3","sheet_x":50,"sheet_y":24},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B3","sheet_x":50,"sheet_y":25},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B3","sheet_x":50,"sheet_y":26},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B3","sheet_x":50,"sheet_y":27}}},{"unified":"1F9D1-200D-1F9BC-200D-27A1-FE0F","sheet_x":50,"sheet_y":28,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BC-200D-27A1-FE0F","sheet_x":50,"sheet_y":29},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BC-200D-27A1-FE0F","sheet_x":50,"sheet_y":30},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BC-200D-27A1-FE0F","sheet_x":50,"sheet_y":31},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BC-200D-27A1-FE0F","sheet_x":50,"sheet_y":32},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BC-200D-27A1-FE0F","sheet_x":50,"sheet_y":33}}},{"unified":"1F9D1-200D-1F9BC","sheet_x":50,"sheet_y":34,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BC","sheet_x":50,"sheet_y":35},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BC","sheet_x":50,"sheet_y":36},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BC","sheet_x":50,"sheet_y":37},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BC","sheet_x":50,"sheet_y":38},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BC","sheet_x":50,"sheet_y":39}}},{"unified":"1F9D1-200D-1F9BD-200D-27A1-FE0F","sheet_x":50,"sheet_y":40,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BD-200D-27A1-FE0F","sheet_x":50,"sheet_y":41},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BD-200D-27A1-FE0F","sheet_x":50,"sheet_y":42},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BD-200D-27A1-FE0F","sheet_x":50,"sheet_y":43},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BD-200D-27A1-FE0F","sheet_x":50,"sheet_y":44},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BD-200D-27A1-FE0F","sheet_x":50,"sheet_y":45}}},{"unified":"1F9D1-200D-1F9BD","sheet_x":50,"sheet_y":46,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BD","sheet_x":50,"sheet_y":47},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BD","sheet_x":50,"sheet_y":48},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BD","sheet_x":50,"sheet_y":49},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BD","sheet_x":50,"sheet_y":50},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BD","sheet_x":50,"sheet_y":51}}},{"unified":"1F9D1-200D-1F9D1-200D-1F9D2","sheet_x":50,"sheet_y":52,"skin_variations":{}},{"unified":"1F9D1-200D-1F9D1-200D-1F9D2-200D-1F9D2","sheet_x":50,"sheet_y":53,"skin_variations":{}},{"unified":"1F9D1-200D-1F9D2-200D-1F9D2","sheet_x":50,"sheet_y":54,"skin_variations":{}},{"unified":"1F9D1-200D-1F9D2","sheet_x":50,"sheet_y":55,"skin_variations":{}},{"unified":"1F9D1-200D-2695-FE0F","sheet_x":50,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2695-FE0F","sheet_x":50,"sheet_y":57},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2695-FE0F","sheet_x":50,"sheet_y":58},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2695-FE0F","sheet_x":50,"sheet_y":59},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2695-FE0F","sheet_x":50,"sheet_y":60},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2695-FE0F","sheet_x":50,"sheet_y":61}}},{"unified":"1F9D1-200D-2696-FE0F","sheet_x":51,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2696-FE0F","sheet_x":51,"sheet_y":1},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2696-FE0F","sheet_x":51,"sheet_y":2},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2696-FE0F","sheet_x":51,"sheet_y":3},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2696-FE0F","sheet_x":51,"sheet_y":4},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2696-FE0F","sheet_x":51,"sheet_y":5}}},{"unified":"1F9D1-200D-2708-FE0F","sheet_x":51,"sheet_y":6,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2708-FE0F","sheet_x":51,"sheet_y":7},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2708-FE0F","sheet_x":51,"sheet_y":8},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2708-FE0F","sheet_x":51,"sheet_y":9},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2708-FE0F","sheet_x":51,"sheet_y":10},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2708-FE0F","sheet_x":51,"sheet_y":11}}},{"unified":"1F9D1","sheet_x":51,"sheet_y":12,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB","sheet_x":51,"sheet_y":13},"1F3FC":{"unified":"1F9D1-1F3FC","sheet_x":51,"sheet_y":14},"1F3FD":{"unified":"1F9D1-1F3FD","sheet_x":51,"sheet_y":15},"1F3FE":{"unified":"1F9D1-1F3FE","sheet_x":51,"sheet_y":16},"1F3FF":{"unified":"1F9D1-1F3FF","sheet_x":51,"sheet_y":17}}},{"unified":"1F9D2","sheet_x":51,"sheet_y":18,"skin_variations":{"1F3FB":{"unified":"1F9D2-1F3FB","sheet_x":51,"sheet_y":19},"1F3FC":{"unified":"1F9D2-1F3FC","sheet_x":51,"sheet_y":20},"1F3FD":{"unified":"1F9D2-1F3FD","sheet_x":51,"sheet_y":21},"1F3FE":{"unified":"1F9D2-1F3FE","sheet_x":51,"sheet_y":22},"1F3FF":{"unified":"1F9D2-1F3FF","sheet_x":51,"sheet_y":23}}},{"unified":"1F9D3","sheet_x":51,"sheet_y":24,"skin_variations":{"1F3FB":{"unified":"1F9D3-1F3FB","sheet_x":51,"sheet_y":25},"1F3FC":{"unified":"1F9D3-1F3FC","sheet_x":51,"sheet_y":26},"1F3FD":{"unified":"1F9D3-1F3FD","sheet_x":51,"sheet_y":27},"1F3FE":{"unified":"1F9D3-1F3FE","sheet_x":51,"sheet_y":28},"1F3FF":{"unified":"1F9D3-1F3FF","sheet_x":51,"sheet_y":29}}},{"unified":"1F9D4-200D-2640-FE0F","sheet_x":51,"sheet_y":30,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB-200D-2640-FE0F","sheet_x":51,"sheet_y":31},"1F3FC":{"unified":"1F9D4-1F3FC-200D-2640-FE0F","sheet_x":51,"sheet_y":32},"1F3FD":{"unified":"1F9D4-1F3FD-200D-2640-FE0F","sheet_x":51,"sheet_y":33},"1F3FE":{"unified":"1F9D4-1F3FE-200D-2640-FE0F","sheet_x":51,"sheet_y":34},"1F3FF":{"unified":"1F9D4-1F3FF-200D-2640-FE0F","sheet_x":51,"sheet_y":35}}},{"unified":"1F9D4-200D-2642-FE0F","sheet_x":51,"sheet_y":36,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB-200D-2642-FE0F","sheet_x":51,"sheet_y":37},"1F3FC":{"unified":"1F9D4-1F3FC-200D-2642-FE0F","sheet_x":51,"sheet_y":38},"1F3FD":{"unified":"1F9D4-1F3FD-200D-2642-FE0F","sheet_x":51,"sheet_y":39},"1F3FE":{"unified":"1F9D4-1F3FE-200D-2642-FE0F","sheet_x":51,"sheet_y":40},"1F3FF":{"unified":"1F9D4-1F3FF-200D-2642-FE0F","sheet_x":51,"sheet_y":41}}},{"unified":"1F9D4","sheet_x":51,"sheet_y":42,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB","sheet_x":51,"sheet_y":43},"1F3FC":{"unified":"1F9D4-1F3FC","sheet_x":51,"sheet_y":44},"1F3FD":{"unified":"1F9D4-1F3FD","sheet_x":51,"sheet_y":45},"1F3FE":{"unified":"1F9D4-1F3FE","sheet_x":51,"sheet_y":46},"1F3FF":{"unified":"1F9D4-1F3FF","sheet_x":51,"sheet_y":47}}},{"unified":"1F9D5","sheet_x":51,"sheet_y":48,"skin_variations":{"1F3FB":{"unified":"1F9D5-1F3FB","sheet_x":51,"sheet_y":49},"1F3FC":{"unified":"1F9D5-1F3FC","sheet_x":51,"sheet_y":50},"1F3FD":{"unified":"1F9D5-1F3FD","sheet_x":51,"sheet_y":51},"1F3FE":{"unified":"1F9D5-1F3FE","sheet_x":51,"sheet_y":52},"1F3FF":{"unified":"1F9D5-1F3FF","sheet_x":51,"sheet_y":53}}},{"unified":"1F9D6-200D-2640-FE0F","sheet_x":51,"sheet_y":54,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB-200D-2640-FE0F","sheet_x":51,"sheet_y":55},"1F3FC":{"unified":"1F9D6-1F3FC-200D-2640-FE0F","sheet_x":51,"sheet_y":56},"1F3FD":{"unified":"1F9D6-1F3FD-200D-2640-FE0F","sheet_x":51,"sheet_y":57},"1F3FE":{"unified":"1F9D6-1F3FE-200D-2640-FE0F","sheet_x":51,"sheet_y":58},"1F3FF":{"unified":"1F9D6-1F3FF-200D-2640-FE0F","sheet_x":51,"sheet_y":59}}},{"unified":"1F9D6-200D-2642-FE0F","sheet_x":51,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB-200D-2642-FE0F","sheet_x":51,"sheet_y":61},"1F3FC":{"unified":"1F9D6-1F3FC-200D-2642-FE0F","sheet_x":52,"sheet_y":0},"1F3FD":{"unified":"1F9D6-1F3FD-200D-2642-FE0F","sheet_x":52,"sheet_y":1},"1F3FE":{"unified":"1F9D6-1F3FE-200D-2642-FE0F","sheet_x":52,"sheet_y":2},"1F3FF":{"unified":"1F9D6-1F3FF-200D-2642-FE0F","sheet_x":52,"sheet_y":3}}},{"unified":"1F9D6","sheet_x":52,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB","sheet_x":52,"sheet_y":5},"1F3FC":{"unified":"1F9D6-1F3FC","sheet_x":52,"sheet_y":6},"1F3FD":{"unified":"1F9D6-1F3FD","sheet_x":52,"sheet_y":7},"1F3FE":{"unified":"1F9D6-1F3FE","sheet_x":52,"sheet_y":8},"1F3FF":{"unified":"1F9D6-1F3FF","sheet_x":52,"sheet_y":9}}},{"unified":"1F9D7-200D-2640-FE0F","sheet_x":52,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB-200D-2640-FE0F","sheet_x":52,"sheet_y":11},"1F3FC":{"unified":"1F9D7-1F3FC-200D-2640-FE0F","sheet_x":52,"sheet_y":12},"1F3FD":{"unified":"1F9D7-1F3FD-200D-2640-FE0F","sheet_x":52,"sheet_y":13},"1F3FE":{"unified":"1F9D7-1F3FE-200D-2640-FE0F","sheet_x":52,"sheet_y":14},"1F3FF":{"unified":"1F9D7-1F3FF-200D-2640-FE0F","sheet_x":52,"sheet_y":15}}},{"unified":"1F9D7-200D-2642-FE0F","sheet_x":52,"sheet_y":16,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB-200D-2642-FE0F","sheet_x":52,"sheet_y":17},"1F3FC":{"unified":"1F9D7-1F3FC-200D-2642-FE0F","sheet_x":52,"sheet_y":18},"1F3FD":{"unified":"1F9D7-1F3FD-200D-2642-FE0F","sheet_x":52,"sheet_y":19},"1F3FE":{"unified":"1F9D7-1F3FE-200D-2642-FE0F","sheet_x":52,"sheet_y":20},"1F3FF":{"unified":"1F9D7-1F3FF-200D-2642-FE0F","sheet_x":52,"sheet_y":21}}},{"unified":"1F9D7","sheet_x":52,"sheet_y":22,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB","sheet_x":52,"sheet_y":23},"1F3FC":{"unified":"1F9D7-1F3FC","sheet_x":52,"sheet_y":24},"1F3FD":{"unified":"1F9D7-1F3FD","sheet_x":52,"sheet_y":25},"1F3FE":{"unified":"1F9D7-1F3FE","sheet_x":52,"sheet_y":26},"1F3FF":{"unified":"1F9D7-1F3FF","sheet_x":52,"sheet_y":27}}},{"unified":"1F9D8-200D-2640-FE0F","sheet_x":52,"sheet_y":28,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB-200D-2640-FE0F","sheet_x":52,"sheet_y":29},"1F3FC":{"unified":"1F9D8-1F3FC-200D-2640-FE0F","sheet_x":52,"sheet_y":30},"1F3FD":{"unified":"1F9D8-1F3FD-200D-2640-FE0F","sheet_x":52,"sheet_y":31},"1F3FE":{"unified":"1F9D8-1F3FE-200D-2640-FE0F","sheet_x":52,"sheet_y":32},"1F3FF":{"unified":"1F9D8-1F3FF-200D-2640-FE0F","sheet_x":52,"sheet_y":33}}},{"unified":"1F9D8-200D-2642-FE0F","sheet_x":52,"sheet_y":34,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB-200D-2642-FE0F","sheet_x":52,"sheet_y":35},"1F3FC":{"unified":"1F9D8-1F3FC-200D-2642-FE0F","sheet_x":52,"sheet_y":36},"1F3FD":{"unified":"1F9D8-1F3FD-200D-2642-FE0F","sheet_x":52,"sheet_y":37},"1F3FE":{"unified":"1F9D8-1F3FE-200D-2642-FE0F","sheet_x":52,"sheet_y":38},"1F3FF":{"unified":"1F9D8-1F3FF-200D-2642-FE0F","sheet_x":52,"sheet_y":39}}},{"unified":"1F9D8","sheet_x":52,"sheet_y":40,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB","sheet_x":52,"sheet_y":41},"1F3FC":{"unified":"1F9D8-1F3FC","sheet_x":52,"sheet_y":42},"1F3FD":{"unified":"1F9D8-1F3FD","sheet_x":52,"sheet_y":43},"1F3FE":{"unified":"1F9D8-1F3FE","sheet_x":52,"sheet_y":44},"1F3FF":{"unified":"1F9D8-1F3FF","sheet_x":52,"sheet_y":45}}},{"unified":"1F9D9-200D-2640-FE0F","sheet_x":52,"sheet_y":46,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB-200D-2640-FE0F","sheet_x":52,"sheet_y":47},"1F3FC":{"unified":"1F9D9-1F3FC-200D-2640-FE0F","sheet_x":52,"sheet_y":48},"1F3FD":{"unified":"1F9D9-1F3FD-200D-2640-FE0F","sheet_x":52,"sheet_y":49},"1F3FE":{"unified":"1F9D9-1F3FE-200D-2640-FE0F","sheet_x":52,"sheet_y":50},"1F3FF":{"unified":"1F9D9-1F3FF-200D-2640-FE0F","sheet_x":52,"sheet_y":51}}},{"unified":"1F9D9-200D-2642-FE0F","sheet_x":52,"sheet_y":52,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB-200D-2642-FE0F","sheet_x":52,"sheet_y":53},"1F3FC":{"unified":"1F9D9-1F3FC-200D-2642-FE0F","sheet_x":52,"sheet_y":54},"1F3FD":{"unified":"1F9D9-1F3FD-200D-2642-FE0F","sheet_x":52,"sheet_y":55},"1F3FE":{"unified":"1F9D9-1F3FE-200D-2642-FE0F","sheet_x":52,"sheet_y":56},"1F3FF":{"unified":"1F9D9-1F3FF-200D-2642-FE0F","sheet_x":52,"sheet_y":57}}},{"unified":"1F9D9","sheet_x":52,"sheet_y":58,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB","sheet_x":52,"sheet_y":59},"1F3FC":{"unified":"1F9D9-1F3FC","sheet_x":52,"sheet_y":60},"1F3FD":{"unified":"1F9D9-1F3FD","sheet_x":52,"sheet_y":61},"1F3FE":{"unified":"1F9D9-1F3FE","sheet_x":53,"sheet_y":0},"1F3FF":{"unified":"1F9D9-1F3FF","sheet_x":53,"sheet_y":1}}},{"unified":"1F9DA-200D-2640-FE0F","sheet_x":53,"sheet_y":2,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB-200D-2640-FE0F","sheet_x":53,"sheet_y":3},"1F3FC":{"unified":"1F9DA-1F3FC-200D-2640-FE0F","sheet_x":53,"sheet_y":4},"1F3FD":{"unified":"1F9DA-1F3FD-200D-2640-FE0F","sheet_x":53,"sheet_y":5},"1F3FE":{"unified":"1F9DA-1F3FE-200D-2640-FE0F","sheet_x":53,"sheet_y":6},"1F3FF":{"unified":"1F9DA-1F3FF-200D-2640-FE0F","sheet_x":53,"sheet_y":7}}},{"unified":"1F9DA-200D-2642-FE0F","sheet_x":53,"sheet_y":8,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB-200D-2642-FE0F","sheet_x":53,"sheet_y":9},"1F3FC":{"unified":"1F9DA-1F3FC-200D-2642-FE0F","sheet_x":53,"sheet_y":10},"1F3FD":{"unified":"1F9DA-1F3FD-200D-2642-FE0F","sheet_x":53,"sheet_y":11},"1F3FE":{"unified":"1F9DA-1F3FE-200D-2642-FE0F","sheet_x":53,"sheet_y":12},"1F3FF":{"unified":"1F9DA-1F3FF-200D-2642-FE0F","sheet_x":53,"sheet_y":13}}},{"unified":"1F9DA","sheet_x":53,"sheet_y":14,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB","sheet_x":53,"sheet_y":15},"1F3FC":{"unified":"1F9DA-1F3FC","sheet_x":53,"sheet_y":16},"1F3FD":{"unified":"1F9DA-1F3FD","sheet_x":53,"sheet_y":17},"1F3FE":{"unified":"1F9DA-1F3FE","sheet_x":53,"sheet_y":18},"1F3FF":{"unified":"1F9DA-1F3FF","sheet_x":53,"sheet_y":19}}},{"unified":"1F9DB-200D-2640-FE0F","sheet_x":53,"sheet_y":20,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB-200D-2640-FE0F","sheet_x":53,"sheet_y":21},"1F3FC":{"unified":"1F9DB-1F3FC-200D-2640-FE0F","sheet_x":53,"sheet_y":22},"1F3FD":{"unified":"1F9DB-1F3FD-200D-2640-FE0F","sheet_x":53,"sheet_y":23},"1F3FE":{"unified":"1F9DB-1F3FE-200D-2640-FE0F","sheet_x":53,"sheet_y":24},"1F3FF":{"unified":"1F9DB-1F3FF-200D-2640-FE0F","sheet_x":53,"sheet_y":25}}},{"unified":"1F9DB-200D-2642-FE0F","sheet_x":53,"sheet_y":26,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB-200D-2642-FE0F","sheet_x":53,"sheet_y":27},"1F3FC":{"unified":"1F9DB-1F3FC-200D-2642-FE0F","sheet_x":53,"sheet_y":28},"1F3FD":{"unified":"1F9DB-1F3FD-200D-2642-FE0F","sheet_x":53,"sheet_y":29},"1F3FE":{"unified":"1F9DB-1F3FE-200D-2642-FE0F","sheet_x":53,"sheet_y":30},"1F3FF":{"unified":"1F9DB-1F3FF-200D-2642-FE0F","sheet_x":53,"sheet_y":31}}},{"unified":"1F9DB","sheet_x":53,"sheet_y":32,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB","sheet_x":53,"sheet_y":33},"1F3FC":{"unified":"1F9DB-1F3FC","sheet_x":53,"sheet_y":34},"1F3FD":{"unified":"1F9DB-1F3FD","sheet_x":53,"sheet_y":35},"1F3FE":{"unified":"1F9DB-1F3FE","sheet_x":53,"sheet_y":36},"1F3FF":{"unified":"1F9DB-1F3FF","sheet_x":53,"sheet_y":37}}},{"unified":"1F9DC-200D-2640-FE0F","sheet_x":53,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB-200D-2640-FE0F","sheet_x":53,"sheet_y":39},"1F3FC":{"unified":"1F9DC-1F3FC-200D-2640-FE0F","sheet_x":53,"sheet_y":40},"1F3FD":{"unified":"1F9DC-1F3FD-200D-2640-FE0F","sheet_x":53,"sheet_y":41},"1F3FE":{"unified":"1F9DC-1F3FE-200D-2640-FE0F","sheet_x":53,"sheet_y":42},"1F3FF":{"unified":"1F9DC-1F3FF-200D-2640-FE0F","sheet_x":53,"sheet_y":43}}},{"unified":"1F9DC-200D-2642-FE0F","sheet_x":53,"sheet_y":44,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB-200D-2642-FE0F","sheet_x":53,"sheet_y":45},"1F3FC":{"unified":"1F9DC-1F3FC-200D-2642-FE0F","sheet_x":53,"sheet_y":46},"1F3FD":{"unified":"1F9DC-1F3FD-200D-2642-FE0F","sheet_x":53,"sheet_y":47},"1F3FE":{"unified":"1F9DC-1F3FE-200D-2642-FE0F","sheet_x":53,"sheet_y":48},"1F3FF":{"unified":"1F9DC-1F3FF-200D-2642-FE0F","sheet_x":53,"sheet_y":49}}},{"unified":"1F9DC","sheet_x":53,"sheet_y":50,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB","sheet_x":53,"sheet_y":51},"1F3FC":{"unified":"1F9DC-1F3FC","sheet_x":53,"sheet_y":52},"1F3FD":{"unified":"1F9DC-1F3FD","sheet_x":53,"sheet_y":53},"1F3FE":{"unified":"1F9DC-1F3FE","sheet_x":53,"sheet_y":54},"1F3FF":{"unified":"1F9DC-1F3FF","sheet_x":53,"sheet_y":55}}},{"unified":"1F9DD-200D-2640-FE0F","sheet_x":53,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB-200D-2640-FE0F","sheet_x":53,"sheet_y":57},"1F3FC":{"unified":"1F9DD-1F3FC-200D-2640-FE0F","sheet_x":53,"sheet_y":58},"1F3FD":{"unified":"1F9DD-1F3FD-200D-2640-FE0F","sheet_x":53,"sheet_y":59},"1F3FE":{"unified":"1F9DD-1F3FE-200D-2640-FE0F","sheet_x":53,"sheet_y":60},"1F3FF":{"unified":"1F9DD-1F3FF-200D-2640-FE0F","sheet_x":53,"sheet_y":61}}},{"unified":"1F9DD-200D-2642-FE0F","sheet_x":54,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB-200D-2642-FE0F","sheet_x":54,"sheet_y":1},"1F3FC":{"unified":"1F9DD-1F3FC-200D-2642-FE0F","sheet_x":54,"sheet_y":2},"1F3FD":{"unified":"1F9DD-1F3FD-200D-2642-FE0F","sheet_x":54,"sheet_y":3},"1F3FE":{"unified":"1F9DD-1F3FE-200D-2642-FE0F","sheet_x":54,"sheet_y":4},"1F3FF":{"unified":"1F9DD-1F3FF-200D-2642-FE0F","sheet_x":54,"sheet_y":5}}},{"unified":"1F9DD","sheet_x":54,"sheet_y":6,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB","sheet_x":54,"sheet_y":7},"1F3FC":{"unified":"1F9DD-1F3FC","sheet_x":54,"sheet_y":8},"1F3FD":{"unified":"1F9DD-1F3FD","sheet_x":54,"sheet_y":9},"1F3FE":{"unified":"1F9DD-1F3FE","sheet_x":54,"sheet_y":10},"1F3FF":{"unified":"1F9DD-1F3FF","sheet_x":54,"sheet_y":11}}},{"unified":"1F9DE-200D-2640-FE0F","sheet_x":54,"sheet_y":12,"skin_variations":{}},{"unified":"1F9DE-200D-2642-FE0F","sheet_x":54,"sheet_y":13,"skin_variations":{}},{"unified":"1F9DE","sheet_x":54,"sheet_y":14,"skin_variations":{}},{"unified":"1F9DF-200D-2640-FE0F","sheet_x":54,"sheet_y":15,"skin_variations":{}},{"unified":"1F9DF-200D-2642-FE0F","sheet_x":54,"sheet_y":16,"skin_variations":{}},{"unified":"1F9DF","sheet_x":54,"sheet_y":17,"skin_variations":{}},{"unified":"1F9E0","sheet_x":54,"sheet_y":18,"skin_variations":{}},{"unified":"1F9E1","sheet_x":54,"sheet_y":19,"skin_variations":{}},{"unified":"1F9E2","sheet_x":54,"sheet_y":20,"skin_variations":{}},{"unified":"1F9E3","sheet_x":54,"sheet_y":21,"skin_variations":{}},{"unified":"1F9E4","sheet_x":54,"sheet_y":22,"skin_variations":{}},{"unified":"1F9E5","sheet_x":54,"sheet_y":23,"skin_variations":{}},{"unified":"1F9E6","sheet_x":54,"sheet_y":24,"skin_variations":{}},{"unified":"1F9E7","sheet_x":54,"sheet_y":25,"skin_variations":{}},{"unified":"1F9E8","sheet_x":54,"sheet_y":26,"skin_variations":{}},{"unified":"1F9E9","sheet_x":54,"sheet_y":27,"skin_variations":{}},{"unified":"1F9EA","sheet_x":54,"sheet_y":28,"skin_variations":{}},{"unified":"1F9EB","sheet_x":54,"sheet_y":29,"skin_variations":{}},{"unified":"1F9EC","sheet_x":54,"sheet_y":30,"skin_variations":{}},{"unified":"1F9ED","sheet_x":54,"sheet_y":31,"skin_variations":{}},{"unified":"1F9EE","sheet_x":54,"sheet_y":32,"skin_variations":{}},{"unified":"1F9EF","sheet_x":54,"sheet_y":33,"skin_variations":{}},{"unified":"1F9F0","sheet_x":54,"sheet_y":34,"skin_variations":{}},{"unified":"1F9F1","sheet_x":54,"sheet_y":35,"skin_variations":{}},{"unified":"1F9F2","sheet_x":54,"sheet_y":36,"skin_variations":{}},{"unified":"1F9F3","sheet_x":54,"sheet_y":37,"skin_variations":{}},{"unified":"1F9F4","sheet_x":54,"sheet_y":38,"skin_variations":{}},{"unified":"1F9F5","sheet_x":54,"sheet_y":39,"skin_variations":{}},{"unified":"1F9F6","sheet_x":54,"sheet_y":40,"skin_variations":{}},{"unified":"1F9F7","sheet_x":54,"sheet_y":41,"skin_variations":{}},{"unified":"1F9F8","sheet_x":54,"sheet_y":42,"skin_variations":{}},{"unified":"1F9F9","sheet_x":54,"sheet_y":43,"skin_variations":{}},{"unified":"1F9FA","sheet_x":54,"sheet_y":44,"skin_variations":{}},{"unified":"1F9FB","sheet_x":54,"sheet_y":45,"skin_variations":{}},{"unified":"1F9FC","sheet_x":54,"sheet_y":46,"skin_variations":{}},{"unified":"1F9FD","sheet_x":54,"sheet_y":47,"skin_variations":{}},{"unified":"1F9FE","sheet_x":54,"sheet_y":48,"skin_variations":{}},{"unified":"1F9FF","sheet_x":54,"sheet_y":49,"skin_variations":{}},{"unified":"1FA70","sheet_x":54,"sheet_y":50,"skin_variations":{}},{"unified":"1FA71","sheet_x":54,"sheet_y":51,"skin_variations":{}},{"unified":"1FA72","sheet_x":54,"sheet_y":52,"skin_variations":{}},{"unified":"1FA73","sheet_x":54,"sheet_y":53,"skin_variations":{}},{"unified":"1FA74","sheet_x":54,"sheet_y":54,"skin_variations":{}},{"unified":"1FA75","sheet_x":54,"sheet_y":55,"skin_variations":{}},{"unified":"1FA76","sheet_x":54,"sheet_y":56,"skin_variations":{}},{"unified":"1FA77","sheet_x":54,"sheet_y":57,"skin_variations":{}},{"unified":"1FA78","sheet_x":54,"sheet_y":58,"skin_variations":{}},{"unified":"1FA79","sheet_x":54,"sheet_y":59,"skin_variations":{}},{"unified":"1FA7A","sheet_x":54,"sheet_y":60,"skin_variations":{}},{"unified":"1FA7B","sheet_x":54,"sheet_y":61,"skin_variations":{}},{"unified":"1FA7C","sheet_x":55,"sheet_y":0,"skin_variations":{}},{"unified":"1FA80","sheet_x":55,"sheet_y":1,"skin_variations":{}},{"unified":"1FA81","sheet_x":55,"sheet_y":2,"skin_variations":{}},{"unified":"1FA82","sheet_x":55,"sheet_y":3,"skin_variations":{}},{"unified":"1FA83","sheet_x":55,"sheet_y":4,"skin_variations":{}},{"unified":"1FA84","sheet_x":55,"sheet_y":5,"skin_variations":{}},{"unified":"1FA85","sheet_x":55,"sheet_y":6,"skin_variations":{}},{"unified":"1FA86","sheet_x":55,"sheet_y":7,"skin_variations":{}},{"unified":"1FA87","sheet_x":55,"sheet_y":8,"skin_variations":{}},{"unified":"1FA88","sheet_x":55,"sheet_y":9,"skin_variations":{}},{"unified":"1FA90","sheet_x":55,"sheet_y":10,"skin_variations":{}},{"unified":"1FA91","sheet_x":55,"sheet_y":11,"skin_variations":{}},{"unified":"1FA92","sheet_x":55,"sheet_y":12,"skin_variations":{}},{"unified":"1FA93","sheet_x":55,"sheet_y":13,"skin_variations":{}},{"unified":"1FA94","sheet_x":55,"sheet_y":14,"skin_variations":{}},{"unified":"1FA95","sheet_x":55,"sheet_y":15,"skin_variations":{}},{"unified":"1FA96","sheet_x":55,"sheet_y":16,"skin_variations":{}},{"unified":"1FA97","sheet_x":55,"sheet_y":17,"skin_variations":{}},{"unified":"1FA98","sheet_x":55,"sheet_y":18,"skin_variations":{}},{"unified":"1FA99","sheet_x":55,"sheet_y":19,"skin_variations":{}},{"unified":"1FA9A","sheet_x":55,"sheet_y":20,"skin_variations":{}},{"unified":"1FA9B","sheet_x":55,"sheet_y":21,"skin_variations":{}},{"unified":"1FA9C","sheet_x":55,"sheet_y":22,"skin_variations":{}},{"unified":"1FA9D","sheet_x":55,"sheet_y":23,"skin_variations":{}},{"unified":"1FA9E","sheet_x":55,"sheet_y":24,"skin_variations":{}},{"unified":"1FA9F","sheet_x":55,"sheet_y":25,"skin_variations":{}},{"unified":"1FAA0","sheet_x":55,"sheet_y":26,"skin_variations":{}},{"unified":"1FAA1","sheet_x":55,"sheet_y":27,"skin_variations":{}},{"unified":"1FAA2","sheet_x":55,"sheet_y":28,"skin_variations":{}},{"unified":"1FAA3","sheet_x":55,"sheet_y":29,"skin_variations":{}},{"unified":"1FAA4","sheet_x":55,"sheet_y":30,"skin_variations":{}},{"unified":"1FAA5","sheet_x":55,"sheet_y":31,"skin_variations":{}},{"unified":"1FAA6","sheet_x":55,"sheet_y":32,"skin_variations":{}},{"unified":"1FAA7","sheet_x":55,"sheet_y":33,"skin_variations":{}},{"unified":"1FAA8","sheet_x":55,"sheet_y":34,"skin_variations":{}},{"unified":"1FAA9","sheet_x":55,"sheet_y":35,"skin_variations":{}},{"unified":"1FAAA","sheet_x":55,"sheet_y":36,"skin_variations":{}},{"unified":"1FAAB","sheet_x":55,"sheet_y":37,"skin_variations":{}},{"unified":"1FAAC","sheet_x":55,"sheet_y":38,"skin_variations":{}},{"unified":"1FAAD","sheet_x":55,"sheet_y":39,"skin_variations":{}},{"unified":"1FAAE","sheet_x":55,"sheet_y":40,"skin_variations":{}},{"unified":"1FAAF","sheet_x":55,"sheet_y":41,"skin_variations":{}},{"unified":"1FAB0","sheet_x":55,"sheet_y":42,"skin_variations":{}},{"unified":"1FAB1","sheet_x":55,"sheet_y":43,"skin_variations":{}},{"unified":"1FAB2","sheet_x":55,"sheet_y":44,"skin_variations":{}},{"unified":"1FAB3","sheet_x":55,"sheet_y":45,"skin_variations":{}},{"unified":"1FAB4","sheet_x":55,"sheet_y":46,"skin_variations":{}},{"unified":"1FAB5","sheet_x":55,"sheet_y":47,"skin_variations":{}},{"unified":"1FAB6","sheet_x":55,"sheet_y":48,"skin_variations":{}},{"unified":"1FAB7","sheet_x":55,"sheet_y":49,"skin_variations":{}},{"unified":"1FAB8","sheet_x":55,"sheet_y":50,"skin_variations":{}},{"unified":"1FAB9","sheet_x":55,"sheet_y":51,"skin_variations":{}},{"unified":"1FABA","sheet_x":55,"sheet_y":52,"skin_variations":{}},{"unified":"1FABB","sheet_x":55,"sheet_y":53,"skin_variations":{}},{"unified":"1FABC","sheet_x":55,"sheet_y":54,"skin_variations":{}},{"unified":"1FABD","sheet_x":55,"sheet_y":55,"skin_variations":{}},{"unified":"1FABF","sheet_x":55,"sheet_y":56,"skin_variations":{}},{"unified":"1FAC0","sheet_x":55,"sheet_y":57,"skin_variations":{}},{"unified":"1FAC1","sheet_x":55,"sheet_y":58,"skin_variations":{}},{"unified":"1FAC2","sheet_x":55,"sheet_y":59,"skin_variations":{}},{"unified":"1FAC3","sheet_x":55,"sheet_y":60,"skin_variations":{"1F3FB":{"unified":"1FAC3-1F3FB","sheet_x":55,"sheet_y":61},"1F3FC":{"unified":"1FAC3-1F3FC","sheet_x":56,"sheet_y":0},"1F3FD":{"unified":"1FAC3-1F3FD","sheet_x":56,"sheet_y":1},"1F3FE":{"unified":"1FAC3-1F3FE","sheet_x":56,"sheet_y":2},"1F3FF":{"unified":"1FAC3-1F3FF","sheet_x":56,"sheet_y":3}}},{"unified":"1FAC4","sheet_x":56,"sheet_y":4,"skin_variations":{"1F3FB":{"unified":"1FAC4-1F3FB","sheet_x":56,"sheet_y":5},"1F3FC":{"unified":"1FAC4-1F3FC","sheet_x":56,"sheet_y":6},"1F3FD":{"unified":"1FAC4-1F3FD","sheet_x":56,"sheet_y":7},"1F3FE":{"unified":"1FAC4-1F3FE","sheet_x":56,"sheet_y":8},"1F3FF":{"unified":"1FAC4-1F3FF","sheet_x":56,"sheet_y":9}}},{"unified":"1FAC5","sheet_x":56,"sheet_y":10,"skin_variations":{"1F3FB":{"unified":"1FAC5-1F3FB","sheet_x":56,"sheet_y":11},"1F3FC":{"unified":"1FAC5-1F3FC","sheet_x":56,"sheet_y":12},"1F3FD":{"unified":"1FAC5-1F3FD","sheet_x":56,"sheet_y":13},"1F3FE":{"unified":"1FAC5-1F3FE","sheet_x":56,"sheet_y":14},"1F3FF":{"unified":"1FAC5-1F3FF","sheet_x":56,"sheet_y":15}}},{"unified":"1FACE","sheet_x":56,"sheet_y":16,"skin_variations":{}},{"unified":"1FACF","sheet_x":56,"sheet_y":17,"skin_variations":{}},{"unified":"1FAD0","sheet_x":56,"sheet_y":18,"skin_variations":{}},{"unified":"1FAD1","sheet_x":56,"sheet_y":19,"skin_variations":{}},{"unified":"1FAD2","sheet_x":56,"sheet_y":20,"skin_variations":{}},{"unified":"1FAD3","sheet_x":56,"sheet_y":21,"skin_variations":{}},{"unified":"1FAD4","sheet_x":56,"sheet_y":22,"skin_variations":{}},{"unified":"1FAD5","sheet_x":56,"sheet_y":23,"skin_variations":{}},{"unified":"1FAD6","sheet_x":56,"sheet_y":24,"skin_variations":{}},{"unified":"1FAD7","sheet_x":56,"sheet_y":25,"skin_variations":{}},{"unified":"1FAD8","sheet_x":56,"sheet_y":26,"skin_variations":{}},{"unified":"1FAD9","sheet_x":56,"sheet_y":27,"skin_variations":{}},{"unified":"1FADA","sheet_x":56,"sheet_y":28,"skin_variations":{}},{"unified":"1FADB","sheet_x":56,"sheet_y":29,"skin_variations":{}},{"unified":"1FAE0","sheet_x":56,"sheet_y":30,"skin_variations":{}},{"unified":"1FAE1","sheet_x":56,"sheet_y":31,"skin_variations":{}},{"unified":"1FAE2","sheet_x":56,"sheet_y":32,"skin_variations":{}},{"unified":"1FAE3","sheet_x":56,"sheet_y":33,"skin_variations":{}},{"unified":"1FAE4","sheet_x":56,"sheet_y":34,"skin_variations":{}},{"unified":"1FAE5","sheet_x":56,"sheet_y":35,"skin_variations":{}},{"unified":"1FAE6","sheet_x":56,"sheet_y":36,"skin_variations":{}},{"unified":"1FAE7","sheet_x":56,"sheet_y":37,"skin_variations":{}},{"unified":"1FAE8","sheet_x":56,"sheet_y":38,"skin_variations":{}},{"unified":"1FAF0","sheet_x":56,"sheet_y":39,"skin_variations":{"1F3FB":{"unified":"1FAF0-1F3FB","sheet_x":56,"sheet_y":40},"1F3FC":{"unified":"1FAF0-1F3FC","sheet_x":56,"sheet_y":41},"1F3FD":{"unified":"1FAF0-1F3FD","sheet_x":56,"sheet_y":42},"1F3FE":{"unified":"1FAF0-1F3FE","sheet_x":56,"sheet_y":43},"1F3FF":{"unified":"1FAF0-1F3FF","sheet_x":56,"sheet_y":44}}},{"unified":"1FAF1","sheet_x":56,"sheet_y":45,"skin_variations":{"1F3FB":{"unified":"1FAF1-1F3FB","sheet_x":56,"sheet_y":46},"1F3FC":{"unified":"1FAF1-1F3FC","sheet_x":56,"sheet_y":47},"1F3FD":{"unified":"1FAF1-1F3FD","sheet_x":56,"sheet_y":48},"1F3FE":{"unified":"1FAF1-1F3FE","sheet_x":56,"sheet_y":49},"1F3FF":{"unified":"1FAF1-1F3FF","sheet_x":56,"sheet_y":50}}},{"unified":"1FAF2","sheet_x":56,"sheet_y":51,"skin_variations":{"1F3FB":{"unified":"1FAF2-1F3FB","sheet_x":56,"sheet_y":52},"1F3FC":{"unified":"1FAF2-1F3FC","sheet_x":56,"sheet_y":53},"1F3FD":{"unified":"1FAF2-1F3FD","sheet_x":56,"sheet_y":54},"1F3FE":{"unified":"1FAF2-1F3FE","sheet_x":56,"sheet_y":55},"1F3FF":{"unified":"1FAF2-1F3FF","sheet_x":56,"sheet_y":56}}},{"unified":"1FAF3","sheet_x":56,"sheet_y":57,"skin_variations":{"1F3FB":{"unified":"1FAF3-1F3FB","sheet_x":56,"sheet_y":58},"1F3FC":{"unified":"1FAF3-1F3FC","sheet_x":56,"sheet_y":59},"1F3FD":{"unified":"1FAF3-1F3FD","sheet_x":56,"sheet_y":60},"1F3FE":{"unified":"1FAF3-1F3FE","sheet_x":56,"sheet_y":61},"1F3FF":{"unified":"1FAF3-1F3FF","sheet_x":57,"sheet_y":0}}},{"unified":"1FAF4","sheet_x":57,"sheet_y":1,"skin_variations":{"1F3FB":{"unified":"1FAF4-1F3FB","sheet_x":57,"sheet_y":2},"1F3FC":{"unified":"1FAF4-1F3FC","sheet_x":57,"sheet_y":3},"1F3FD":{"unified":"1FAF4-1F3FD","sheet_x":57,"sheet_y":4},"1F3FE":{"unified":"1FAF4-1F3FE","sheet_x":57,"sheet_y":5},"1F3FF":{"unified":"1FAF4-1F3FF","sheet_x":57,"sheet_y":6}}},{"unified":"1FAF5","sheet_x":57,"sheet_y":7,"skin_variations":{"1F3FB":{"unified":"1FAF5-1F3FB","sheet_x":57,"sheet_y":8},"1F3FC":{"unified":"1FAF5-1F3FC","sheet_x":57,"sheet_y":9},"1F3FD":{"unified":"1FAF5-1F3FD","sheet_x":57,"sheet_y":10},"1F3FE":{"unified":"1FAF5-1F3FE","sheet_x":57,"sheet_y":11},"1F3FF":{"unified":"1FAF5-1F3FF","sheet_x":57,"sheet_y":12}}},{"unified":"1FAF6","sheet_x":57,"sheet_y":13,"skin_variations":{"1F3FB":{"unified":"1FAF6-1F3FB","sheet_x":57,"sheet_y":14},"1F3FC":{"unified":"1FAF6-1F3FC","sheet_x":57,"sheet_y":15},"1F3FD":{"unified":"1FAF6-1F3FD","sheet_x":57,"sheet_y":16},"1F3FE":{"unified":"1FAF6-1F3FE","sheet_x":57,"sheet_y":17},"1F3FF":{"unified":"1FAF6-1F3FF","sheet_x":57,"sheet_y":18}}},{"unified":"1FAF7","sheet_x":57,"sheet_y":19,"skin_variations":{"1F3FB":{"unified":"1FAF7-1F3FB","sheet_x":57,"sheet_y":20},"1F3FC":{"unified":"1FAF7-1F3FC","sheet_x":57,"sheet_y":21},"1F3FD":{"unified":"1FAF7-1F3FD","sheet_x":57,"sheet_y":22},"1F3FE":{"unified":"1FAF7-1F3FE","sheet_x":57,"sheet_y":23},"1F3FF":{"unified":"1FAF7-1F3FF","sheet_x":57,"sheet_y":24}}},{"unified":"1FAF8","sheet_x":57,"sheet_y":25,"skin_variations":{"1F3FB":{"unified":"1FAF8-1F3FB","sheet_x":57,"sheet_y":26},"1F3FC":{"unified":"1FAF8-1F3FC","sheet_x":57,"sheet_y":27},"1F3FD":{"unified":"1FAF8-1F3FD","sheet_x":57,"sheet_y":28},"1F3FE":{"unified":"1FAF8-1F3FE","sheet_x":57,"sheet_y":29},"1F3FF":{"unified":"1FAF8-1F3FF","sheet_x":57,"sheet_y":30}}},{"unified":"203C-FE0F","sheet_x":57,"sheet_y":31,"skin_variations":{}},{"unified":"2049-FE0F","sheet_x":57,"sheet_y":32,"skin_variations":{}},{"unified":"2122-FE0F","sheet_x":57,"sheet_y":33,"skin_variations":{}},{"unified":"2139-FE0F","sheet_x":57,"sheet_y":34,"skin_variations":{}},{"unified":"2194-FE0F","sheet_x":57,"sheet_y":35,"skin_variations":{}},{"unified":"2195-FE0F","sheet_x":57,"sheet_y":36,"skin_variations":{}},{"unified":"2196-FE0F","sheet_x":57,"sheet_y":37,"skin_variations":{}},{"unified":"2197-FE0F","sheet_x":57,"sheet_y":38,"skin_variations":{}},{"unified":"2198-FE0F","sheet_x":57,"sheet_y":39,"skin_variations":{}},{"unified":"2199-FE0F","sheet_x":57,"sheet_y":40,"skin_variations":{}},{"unified":"21A9-FE0F","sheet_x":57,"sheet_y":41,"skin_variations":{}},{"unified":"21AA-FE0F","sheet_x":57,"sheet_y":42,"skin_variations":{}},{"unified":"231A","sheet_x":57,"sheet_y":43,"skin_variations":{}},{"unified":"231B","sheet_x":57,"sheet_y":44,"skin_variations":{}},{"unified":"2328-FE0F","sheet_x":57,"sheet_y":45,"skin_variations":{}},{"unified":"23CF-FE0F","sheet_x":57,"sheet_y":46,"skin_variations":{}},{"unified":"23E9","sheet_x":57,"sheet_y":47,"skin_variations":{}},{"unified":"23EA","sheet_x":57,"sheet_y":48,"skin_variations":{}},{"unified":"23EB","sheet_x":57,"sheet_y":49,"skin_variations":{}},{"unified":"23EC","sheet_x":57,"sheet_y":50,"skin_variations":{}},{"unified":"23ED-FE0F","sheet_x":57,"sheet_y":51,"skin_variations":{}},{"unified":"23EE-FE0F","sheet_x":57,"sheet_y":52,"skin_variations":{}},{"unified":"23EF-FE0F","sheet_x":57,"sheet_y":53,"skin_variations":{}},{"unified":"23F0","sheet_x":57,"sheet_y":54,"skin_variations":{}},{"unified":"23F1-FE0F","sheet_x":57,"sheet_y":55,"skin_variations":{}},{"unified":"23F2-FE0F","sheet_x":57,"sheet_y":56,"skin_variations":{}},{"unified":"23F3","sheet_x":57,"sheet_y":57,"skin_variations":{}},{"unified":"23F8-FE0F","sheet_x":57,"sheet_y":58,"skin_variations":{}},{"unified":"23F9-FE0F","sheet_x":57,"sheet_y":59,"skin_variations":{}},{"unified":"23FA-FE0F","sheet_x":57,"sheet_y":60,"skin_variations":{}},{"unified":"24C2-FE0F","sheet_x":57,"sheet_y":61,"skin_variations":{}},{"unified":"25AA-FE0F","sheet_x":58,"sheet_y":0,"skin_variations":{}},{"unified":"25AB-FE0F","sheet_x":58,"sheet_y":1,"skin_variations":{}},{"unified":"25B6-FE0F","sheet_x":58,"sheet_y":2,"skin_variations":{}},{"unified":"25C0-FE0F","sheet_x":58,"sheet_y":3,"skin_variations":{}},{"unified":"25FB-FE0F","sheet_x":58,"sheet_y":4,"skin_variations":{}},{"unified":"25FC-FE0F","sheet_x":58,"sheet_y":5,"skin_variations":{}},{"unified":"25FD","sheet_x":58,"sheet_y":6,"skin_variations":{}},{"unified":"25FE","sheet_x":58,"sheet_y":7,"skin_variations":{}},{"unified":"2600-FE0F","sheet_x":58,"sheet_y":8,"skin_variations":{}},{"unified":"2601-FE0F","sheet_x":58,"sheet_y":9,"skin_variations":{}},{"unified":"2602-FE0F","sheet_x":58,"sheet_y":10,"skin_variations":{}},{"unified":"2603-FE0F","sheet_x":58,"sheet_y":11,"skin_variations":{}},{"unified":"2604-FE0F","sheet_x":58,"sheet_y":12,"skin_variations":{}},{"unified":"260E-FE0F","sheet_x":58,"sheet_y":13,"skin_variations":{}},{"unified":"2611-FE0F","sheet_x":58,"sheet_y":14,"skin_variations":{}},{"unified":"2614","sheet_x":58,"sheet_y":15,"skin_variations":{}},{"unified":"2615","sheet_x":58,"sheet_y":16,"skin_variations":{}},{"unified":"2618-FE0F","sheet_x":58,"sheet_y":17,"skin_variations":{}},{"unified":"261D-FE0F","sheet_x":58,"sheet_y":18,"skin_variations":{"1F3FB":{"unified":"261D-1F3FB","sheet_x":58,"sheet_y":19},"1F3FC":{"unified":"261D-1F3FC","sheet_x":58,"sheet_y":20},"1F3FD":{"unified":"261D-1F3FD","sheet_x":58,"sheet_y":21},"1F3FE":{"unified":"261D-1F3FE","sheet_x":58,"sheet_y":22},"1F3FF":{"unified":"261D-1F3FF","sheet_x":58,"sheet_y":23}}},{"unified":"2620-FE0F","sheet_x":58,"sheet_y":24,"skin_variations":{}},{"unified":"2622-FE0F","sheet_x":58,"sheet_y":25,"skin_variations":{}},{"unified":"2623-FE0F","sheet_x":58,"sheet_y":26,"skin_variations":{}},{"unified":"2626-FE0F","sheet_x":58,"sheet_y":27,"skin_variations":{}},{"unified":"262A-FE0F","sheet_x":58,"sheet_y":28,"skin_variations":{}},{"unified":"262E-FE0F","sheet_x":58,"sheet_y":29,"skin_variations":{}},{"unified":"262F-FE0F","sheet_x":58,"sheet_y":30,"skin_variations":{}},{"unified":"2638-FE0F","sheet_x":58,"sheet_y":31,"skin_variations":{}},{"unified":"2639-FE0F","sheet_x":58,"sheet_y":32,"skin_variations":{}},{"unified":"263A-FE0F","sheet_x":58,"sheet_y":33,"skin_variations":{}},{"unified":"2640-FE0F","sheet_x":58,"sheet_y":34,"skin_variations":{}},{"unified":"2642-FE0F","sheet_x":58,"sheet_y":35,"skin_variations":{}},{"unified":"2648","sheet_x":58,"sheet_y":36,"skin_variations":{}},{"unified":"2649","sheet_x":58,"sheet_y":37,"skin_variations":{}},{"unified":"264A","sheet_x":58,"sheet_y":38,"skin_variations":{}},{"unified":"264B","sheet_x":58,"sheet_y":39,"skin_variations":{}},{"unified":"264C","sheet_x":58,"sheet_y":40,"skin_variations":{}},{"unified":"264D","sheet_x":58,"sheet_y":41,"skin_variations":{}},{"unified":"264E","sheet_x":58,"sheet_y":42,"skin_variations":{}},{"unified":"264F","sheet_x":58,"sheet_y":43,"skin_variations":{}},{"unified":"2650","sheet_x":58,"sheet_y":44,"skin_variations":{}},{"unified":"2651","sheet_x":58,"sheet_y":45,"skin_variations":{}},{"unified":"2652","sheet_x":58,"sheet_y":46,"skin_variations":{}},{"unified":"2653","sheet_x":58,"sheet_y":47,"skin_variations":{}},{"unified":"265F-FE0F","sheet_x":58,"sheet_y":48,"skin_variations":{}},{"unified":"2660-FE0F","sheet_x":58,"sheet_y":49,"skin_variations":{}},{"unified":"2663-FE0F","sheet_x":58,"sheet_y":50,"skin_variations":{}},{"unified":"2665-FE0F","sheet_x":58,"sheet_y":51,"skin_variations":{}},{"unified":"2666-FE0F","sheet_x":58,"sheet_y":52,"skin_variations":{}},{"unified":"2668-FE0F","sheet_x":58,"sheet_y":53,"skin_variations":{}},{"unified":"267B-FE0F","sheet_x":58,"sheet_y":54,"skin_variations":{}},{"unified":"267E-FE0F","sheet_x":58,"sheet_y":55,"skin_variations":{}},{"unified":"267F","sheet_x":58,"sheet_y":56,"skin_variations":{}},{"unified":"2692-FE0F","sheet_x":58,"sheet_y":57,"skin_variations":{}},{"unified":"2693","sheet_x":58,"sheet_y":58,"skin_variations":{}},{"unified":"2694-FE0F","sheet_x":58,"sheet_y":59,"skin_variations":{}},{"unified":"2695-FE0F","sheet_x":58,"sheet_y":60,"skin_variations":{}},{"unified":"2696-FE0F","sheet_x":58,"sheet_y":61,"skin_variations":{}},{"unified":"2697-FE0F","sheet_x":59,"sheet_y":0,"skin_variations":{}},{"unified":"2699-FE0F","sheet_x":59,"sheet_y":1,"skin_variations":{}},{"unified":"269B-FE0F","sheet_x":59,"sheet_y":2,"skin_variations":{}},{"unified":"269C-FE0F","sheet_x":59,"sheet_y":3,"skin_variations":{}},{"unified":"26A0-FE0F","sheet_x":59,"sheet_y":4,"skin_variations":{}},{"unified":"26A1","sheet_x":59,"sheet_y":5,"skin_variations":{}},{"unified":"26A7-FE0F","sheet_x":59,"sheet_y":6,"skin_variations":{}},{"unified":"26AA","sheet_x":59,"sheet_y":7,"skin_variations":{}},{"unified":"26AB","sheet_x":59,"sheet_y":8,"skin_variations":{}},{"unified":"26B0-FE0F","sheet_x":59,"sheet_y":9,"skin_variations":{}},{"unified":"26B1-FE0F","sheet_x":59,"sheet_y":10,"skin_variations":{}},{"unified":"26BD","sheet_x":59,"sheet_y":11,"skin_variations":{}},{"unified":"26BE","sheet_x":59,"sheet_y":12,"skin_variations":{}},{"unified":"26C4","sheet_x":59,"sheet_y":13,"skin_variations":{}},{"unified":"26C5","sheet_x":59,"sheet_y":14,"skin_variations":{}},{"unified":"26C8-FE0F","sheet_x":59,"sheet_y":15,"skin_variations":{}},{"unified":"26CE","sheet_x":59,"sheet_y":16,"skin_variations":{}},{"unified":"26CF-FE0F","sheet_x":59,"sheet_y":17,"skin_variations":{}},{"unified":"26D1-FE0F","sheet_x":59,"sheet_y":18,"skin_variations":{}},{"unified":"26D3-FE0F-200D-1F4A5","sheet_x":59,"sheet_y":19,"skin_variations":{}},{"unified":"26D3-FE0F","sheet_x":59,"sheet_y":20,"skin_variations":{}},{"unified":"26D4","sheet_x":59,"sheet_y":21,"skin_variations":{}},{"unified":"26E9-FE0F","sheet_x":59,"sheet_y":22,"skin_variations":{}},{"unified":"26EA","sheet_x":59,"sheet_y":23,"skin_variations":{}},{"unified":"26F0-FE0F","sheet_x":59,"sheet_y":24,"skin_variations":{}},{"unified":"26F1-FE0F","sheet_x":59,"sheet_y":25,"skin_variations":{}},{"unified":"26F2","sheet_x":59,"sheet_y":26,"skin_variations":{}},{"unified":"26F3","sheet_x":59,"sheet_y":27,"skin_variations":{}},{"unified":"26F4-FE0F","sheet_x":59,"sheet_y":28,"skin_variations":{}},{"unified":"26F5","sheet_x":59,"sheet_y":29,"skin_variations":{}},{"unified":"26F7-FE0F","sheet_x":59,"sheet_y":30,"skin_variations":{}},{"unified":"26F8-FE0F","sheet_x":59,"sheet_y":31,"skin_variations":{}},{"unified":"26F9-FE0F-200D-2640-FE0F","sheet_x":59,"sheet_y":32,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB-200D-2640-FE0F","sheet_x":59,"sheet_y":33},"1F3FC":{"unified":"26F9-1F3FC-200D-2640-FE0F","sheet_x":59,"sheet_y":34},"1F3FD":{"unified":"26F9-1F3FD-200D-2640-FE0F","sheet_x":59,"sheet_y":35},"1F3FE":{"unified":"26F9-1F3FE-200D-2640-FE0F","sheet_x":59,"sheet_y":36},"1F3FF":{"unified":"26F9-1F3FF-200D-2640-FE0F","sheet_x":59,"sheet_y":37}}},{"unified":"26F9-FE0F-200D-2642-FE0F","sheet_x":59,"sheet_y":38,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB-200D-2642-FE0F","sheet_x":59,"sheet_y":39},"1F3FC":{"unified":"26F9-1F3FC-200D-2642-FE0F","sheet_x":59,"sheet_y":40},"1F3FD":{"unified":"26F9-1F3FD-200D-2642-FE0F","sheet_x":59,"sheet_y":41},"1F3FE":{"unified":"26F9-1F3FE-200D-2642-FE0F","sheet_x":59,"sheet_y":42},"1F3FF":{"unified":"26F9-1F3FF-200D-2642-FE0F","sheet_x":59,"sheet_y":43}}},{"unified":"26F9-FE0F","sheet_x":59,"sheet_y":44,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB","sheet_x":59,"sheet_y":45},"1F3FC":{"unified":"26F9-1F3FC","sheet_x":59,"sheet_y":46},"1F3FD":{"unified":"26F9-1F3FD","sheet_x":59,"sheet_y":47},"1F3FE":{"unified":"26F9-1F3FE","sheet_x":59,"sheet_y":48},"1F3FF":{"unified":"26F9-1F3FF","sheet_x":59,"sheet_y":49}}},{"unified":"26FA","sheet_x":59,"sheet_y":50,"skin_variations":{}},{"unified":"26FD","sheet_x":59,"sheet_y":51,"skin_variations":{}},{"unified":"2702-FE0F","sheet_x":59,"sheet_y":52,"skin_variations":{}},{"unified":"2705","sheet_x":59,"sheet_y":53,"skin_variations":{}},{"unified":"2708-FE0F","sheet_x":59,"sheet_y":54,"skin_variations":{}},{"unified":"2709-FE0F","sheet_x":59,"sheet_y":55,"skin_variations":{}},{"unified":"270A","sheet_x":59,"sheet_y":56,"skin_variations":{"1F3FB":{"unified":"270A-1F3FB","sheet_x":59,"sheet_y":57},"1F3FC":{"unified":"270A-1F3FC","sheet_x":59,"sheet_y":58},"1F3FD":{"unified":"270A-1F3FD","sheet_x":59,"sheet_y":59},"1F3FE":{"unified":"270A-1F3FE","sheet_x":59,"sheet_y":60},"1F3FF":{"unified":"270A-1F3FF","sheet_x":59,"sheet_y":61}}},{"unified":"270B","sheet_x":60,"sheet_y":0,"skin_variations":{"1F3FB":{"unified":"270B-1F3FB","sheet_x":60,"sheet_y":1},"1F3FC":{"unified":"270B-1F3FC","sheet_x":60,"sheet_y":2},"1F3FD":{"unified":"270B-1F3FD","sheet_x":60,"sheet_y":3},"1F3FE":{"unified":"270B-1F3FE","sheet_x":60,"sheet_y":4},"1F3FF":{"unified":"270B-1F3FF","sheet_x":60,"sheet_y":5}}},{"unified":"270C-FE0F","sheet_x":60,"sheet_y":6,"skin_variations":{"1F3FB":{"unified":"270C-1F3FB","sheet_x":60,"sheet_y":7},"1F3FC":{"unified":"270C-1F3FC","sheet_x":60,"sheet_y":8},"1F3FD":{"unified":"270C-1F3FD","sheet_x":60,"sheet_y":9},"1F3FE":{"unified":"270C-1F3FE","sheet_x":60,"sheet_y":10},"1F3FF":{"unified":"270C-1F3FF","sheet_x":60,"sheet_y":11}}},{"unified":"270D-FE0F","sheet_x":60,"sheet_y":12,"skin_variations":{"1F3FB":{"unified":"270D-1F3FB","sheet_x":60,"sheet_y":13},"1F3FC":{"unified":"270D-1F3FC","sheet_x":60,"sheet_y":14},"1F3FD":{"unified":"270D-1F3FD","sheet_x":60,"sheet_y":15},"1F3FE":{"unified":"270D-1F3FE","sheet_x":60,"sheet_y":16},"1F3FF":{"unified":"270D-1F3FF","sheet_x":60,"sheet_y":17}}},{"unified":"270F-FE0F","sheet_x":60,"sheet_y":18,"skin_variations":{}},{"unified":"2712-FE0F","sheet_x":60,"sheet_y":19,"skin_variations":{}},{"unified":"2714-FE0F","sheet_x":60,"sheet_y":20,"skin_variations":{}},{"unified":"2716-FE0F","sheet_x":60,"sheet_y":21,"skin_variations":{}},{"unified":"271D-FE0F","sheet_x":60,"sheet_y":22,"skin_variations":{}},{"unified":"2721-FE0F","sheet_x":60,"sheet_y":23,"skin_variations":{}},{"unified":"2728","sheet_x":60,"sheet_y":24,"skin_variations":{}},{"unified":"2733-FE0F","sheet_x":60,"sheet_y":25,"skin_variations":{}},{"unified":"2734-FE0F","sheet_x":60,"sheet_y":26,"skin_variations":{}},{"unified":"2744-FE0F","sheet_x":60,"sheet_y":27,"skin_variations":{}},{"unified":"2747-FE0F","sheet_x":60,"sheet_y":28,"skin_variations":{}},{"unified":"274C","sheet_x":60,"sheet_y":29,"skin_variations":{}},{"unified":"274E","sheet_x":60,"sheet_y":30,"skin_variations":{}},{"unified":"2753","sheet_x":60,"sheet_y":31,"skin_variations":{}},{"unified":"2754","sheet_x":60,"sheet_y":32,"skin_variations":{}},{"unified":"2755","sheet_x":60,"sheet_y":33,"skin_variations":{}},{"unified":"2757","sheet_x":60,"sheet_y":34,"skin_variations":{}},{"unified":"2763-FE0F","sheet_x":60,"sheet_y":35,"skin_variations":{}},{"unified":"2764-FE0F-200D-1F525","sheet_x":60,"sheet_y":36,"skin_variations":{}},{"unified":"2764-FE0F-200D-1FA79","sheet_x":60,"sheet_y":37,"skin_variations":{}},{"unified":"2764-FE0F","sheet_x":60,"sheet_y":38,"skin_variations":{}},{"unified":"2795","sheet_x":60,"sheet_y":39,"skin_variations":{}},{"unified":"2796","sheet_x":60,"sheet_y":40,"skin_variations":{}},{"unified":"2797","sheet_x":60,"sheet_y":41,"skin_variations":{}},{"unified":"27A1-FE0F","sheet_x":60,"sheet_y":42,"skin_variations":{}},{"unified":"27B0","sheet_x":60,"sheet_y":43,"skin_variations":{}},{"unified":"27BF","sheet_x":60,"sheet_y":44,"skin_variations":{}},{"unified":"2934-FE0F","sheet_x":60,"sheet_y":45,"skin_variations":{}},{"unified":"2935-FE0F","sheet_x":60,"sheet_y":46,"skin_variations":{}},{"unified":"2B05-FE0F","sheet_x":60,"sheet_y":47,"skin_variations":{}},{"unified":"2B06-FE0F","sheet_x":60,"sheet_y":48,"skin_variations":{}},{"unified":"2B07-FE0F","sheet_x":60,"sheet_y":49,"skin_variations":{}},{"unified":"2B1B","sheet_x":60,"sheet_y":50,"skin_variations":{}},{"unified":"2B1C","sheet_x":60,"sheet_y":51,"skin_variations":{}},{"unified":"2B50","sheet_x":60,"sheet_y":52,"skin_variations":{}},{"unified":"2B55","sheet_x":60,"sheet_y":53,"skin_variations":{}},{"unified":"3030-FE0F","sheet_x":60,"sheet_y":54,"skin_variations":{}},{"unified":"303D-FE0F","sheet_x":60,"sheet_y":55,"skin_variations":{}},{"unified":"3297-FE0F","sheet_x":60,"sheet_y":56,"skin_variations":{}},{"unified":"3299-FE0F","sheet_x":60,"sheet_y":57,"skin_variations":{}}]
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts b/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts
index d116c6c62c..0a5a4c1d76 100644
--- a/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts
+++ b/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts
@@ -9,12 +9,13 @@ import type {
 import emojiCompressed from './emoji_compressed';
 import { unicodeToFilename } from './unicode_to_filename';
 
-type UnicodeMapping = {
-  [key in FilenameData[number][0]]: {
+type UnicodeMapping = Record<
+  FilenameData[number][0],
+  {
     shortCode: ShortCodesToEmojiDataKey;
     filename: FilenameData[number][number];
-  };
-};
+  }
+>;
 
 const [
   shortCodesToEmojiData,
@@ -32,11 +33,8 @@ function processEmojiMapData(
   shortCode?: ShortCodesToEmojiDataKey,
 ) {
   const [native, _filename] = emojiMapData;
-  let filename = emojiMapData[1];
-  if (!filename) {
-    // filename name can be derived from unicodeToFilename
-    filename = unicodeToFilename(native);
-  }
+  // filename name can be derived from unicodeToFilename
+  const filename = emojiMapData[1] ?? unicodeToFilename(native);
   unicodeMapping[native] = {
     shortCode,
     filename,
diff --git a/app/javascript/mastodon/features/emoji/unicode_to_filename_s.js b/app/javascript/mastodon/features/emoji/unicode_to_filename_s.js
index 3395c77174..d00563ea6f 100644
--- a/app/javascript/mastodon/features/emoji/unicode_to_filename_s.js
+++ b/app/javascript/mastodon/features/emoji/unicode_to_filename_s.js
@@ -1,8 +1,6 @@
-/* eslint-disable import/no-commonjs --
-   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */
-
 // taken from:
 // https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
+// eslint-disable-next-line import/no-commonjs, no-undef
 exports.unicodeToFilename = (str) => {
   let result = '';
   let charCode = 0;
diff --git a/app/javascript/mastodon/features/emoji/unicode_to_unified_name_s.js b/app/javascript/mastodon/features/emoji/unicode_to_unified_name_s.js
index 108b911222..f5dc6431a4 100644
--- a/app/javascript/mastodon/features/emoji/unicode_to_unified_name_s.js
+++ b/app/javascript/mastodon/features/emoji/unicode_to_unified_name_s.js
@@ -1,6 +1,3 @@
-/* eslint-disable import/no-commonjs --
-   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */
-
 function padLeft(str, num) {
   while (str.length < num) {
     str = '0' + str;
@@ -9,6 +6,7 @@ function padLeft(str, num) {
   return str;
 }
 
+// eslint-disable-next-line import/no-commonjs, no-undef
 exports.unicodeToUnifiedName = (str) => {
   let output = '';
 
diff --git a/app/javascript/mastodon/features/emoji_reacted_statuses/index.jsx b/app/javascript/mastodon/features/emoji_reacted_statuses/index.jsx
deleted file mode 100644
index 54f19cb7d0..0000000000
--- a/app/javascript/mastodon/features/emoji_reacted_statuses/index.jsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import { Helmet } from 'react-helmet';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { debounce } from 'lodash';
-
-import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
-import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
-import { fetchEmojiReactedStatuses, expandEmojiReactedStatuses } from 'mastodon/actions/emoji_reactions';
-import Column from 'mastodon/components/column';
-import ColumnHeader from 'mastodon/components/column_header';
-import StatusList from 'mastodon/components/status_list';
-
-const messages = defineMessages({
-  heading: { id: 'column.emoji_reactions', defaultMessage: 'Stamps' },
-});
-
-const mapStateToProps = state => ({
-  statusIds: state.getIn(['status_lists', 'emoji_reactions', 'items']),
-  isLoading: state.getIn(['status_lists', 'emoji_reactions', 'isLoading'], true),
-  hasMore: !!state.getIn(['status_lists', 'emoji_reactions', 'next']),
-});
-
-class EmojiReactions extends ImmutablePureComponent {
-
-  static propTypes = {
-    dispatch: PropTypes.func.isRequired,
-    statusIds: ImmutablePropTypes.list.isRequired,
-    intl: PropTypes.object.isRequired,
-    columnId: PropTypes.string,
-    multiColumn: PropTypes.bool,
-    hasMore: PropTypes.bool,
-    isLoading: PropTypes.bool,
-  };
-
-  componentWillMount () {
-    this.props.dispatch(fetchEmojiReactedStatuses());
-  }
-
-  handlePin = () => {
-    const { columnId, dispatch } = this.props;
-
-    if (columnId) {
-      dispatch(removeColumn(columnId));
-    } else {
-      dispatch(addColumn('EMOJI_REACTIONS', {}));
-    }
-  };
-
-  handleMove = (dir) => {
-    const { columnId, dispatch } = this.props;
-    dispatch(moveColumn(columnId, dir));
-  };
-
-  handleHeaderClick = () => {
-    this.column.scrollTop();
-  };
-
-  setRef = c => {
-    this.column = c;
-  };
-
-  handleLoadMore = debounce(() => {
-    this.props.dispatch(expandEmojiReactedStatuses());
-  }, 300, { leading: true });
-
-  render () {
-    const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
-    const pinned = !!columnId;
-
-    const emptyMessage = <FormattedMessage id='empty_column.emoji_reacted_statuses' defaultMessage="You don't have any emoji reacted posts yet. When you emoji react one, it will show up here." />;
-
-    return (
-      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
-        <ColumnHeader
-          icon='smile-o'
-          iconComponent={EmojiReactionIcon}
-          title={intl.formatMessage(messages.heading)}
-          onPin={this.handlePin}
-          onMove={this.handleMove}
-          onClick={this.handleHeaderClick}
-          pinned={pinned}
-          multiColumn={multiColumn}
-          showBackButton
-        />
-
-        <StatusList
-          trackScroll={!pinned}
-          statusIds={statusIds}
-          scrollKey={`emoji_reacted_statuses-${columnId}`}
-          hasMore={hasMore}
-          isLoading={isLoading}
-          onLoadMore={this.handleLoadMore}
-          emptyMessage={emptyMessage}
-          bindToDocument={!multiColumn}
-        />
-
-        <Helmet>
-          <title>{intl.formatMessage(messages.heading)}</title>
-          <meta name='robots' content='noindex' />
-        </Helmet>
-      </Column>
-    );
-  }
-
-}
-
-export default connect(mapStateToProps)(injectIntl(EmojiReactions));
diff --git a/app/javascript/mastodon/features/emoji_reacted_statuses/index.tsx b/app/javascript/mastodon/features/emoji_reacted_statuses/index.tsx
new file mode 100644
index 0000000000..58186cbbd7
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji_reacted_statuses/index.tsx
@@ -0,0 +1,118 @@
+import { useEffect, useRef, useCallback } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import {
+  fetchEmojiReactedStatuses,
+  expandEmojiReactedStatuses,
+} from 'mastodon/actions/emoji_reactions';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusList from 'mastodon/components/status_list';
+import { getStatusList } from 'mastodon/selectors';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const messages = defineMessages({
+  heading: { id: 'column.emoji_reactions', defaultMessage: 'Stamps' },
+});
+
+const Favourites: React.FC<{ columnId: string; multiColumn: boolean }> = ({
+  columnId,
+  multiColumn,
+}) => {
+  const dispatch = useAppDispatch();
+  const intl = useIntl();
+  const columnRef = useRef<ColumnRef>(null);
+  const statusIds = useAppSelector((state) =>
+    getStatusList(state, 'emoji_reactions'),
+  );
+  const isLoading = useAppSelector(
+    (state) =>
+      state.status_lists.getIn(
+        ['emoji_reactions', 'isLoading'],
+        true,
+      ) as boolean,
+  );
+  const hasMore = useAppSelector(
+    (state) => !!state.status_lists.getIn(['emoji_reactions', 'next']),
+  );
+
+  useEffect(() => {
+    dispatch(fetchEmojiReactedStatuses());
+  }, [dispatch]);
+
+  const handlePin = useCallback(() => {
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('EMOJI_REACTIONS', {}));
+    }
+  }, [dispatch, columnId]);
+
+  const handleMove = useCallback(
+    (dir: number) => {
+      dispatch(moveColumn(columnId, dir));
+    },
+    [dispatch, columnId],
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const handleLoadMore = useCallback(() => {
+    dispatch(expandEmojiReactedStatuses());
+  }, [dispatch]);
+
+  const pinned = !!columnId;
+
+  const emptyMessage = (
+    <FormattedMessage
+      id='empty_column.emoji_reacted_statuses'
+      defaultMessage="You don't have any emoji reacted posts yet. When you emoji react one, it will show up here."
+    />
+  );
+
+  return (
+    <Column
+      bindToDocument={!multiColumn}
+      ref={columnRef}
+      label={intl.formatMessage(messages.heading)}
+    >
+      <ColumnHeader
+        icon='star'
+        iconComponent={EmojiReactionIcon}
+        title={intl.formatMessage(messages.heading)}
+        onPin={handlePin}
+        onMove={handleMove}
+        onClick={handleHeaderClick}
+        pinned={pinned}
+        multiColumn={multiColumn}
+      />
+
+      <StatusList
+        trackScroll={!pinned}
+        statusIds={statusIds}
+        scrollKey={`emoji_reacted_statuses-${columnId}`}
+        hasMore={hasMore}
+        isLoading={isLoading}
+        onLoadMore={handleLoadMore}
+        emptyMessage={emptyMessage}
+        bindToDocument={!multiColumn}
+      />
+
+      <Helmet>
+        <title>{intl.formatMessage(messages.heading)}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default Favourites;
diff --git a/app/javascript/mastodon/features/explore/components/card.jsx b/app/javascript/mastodon/features/explore/components/card.jsx
index 15470ec24c..9617781b53 100644
--- a/app/javascript/mastodon/features/explore/components/card.jsx
+++ b/app/javascript/mastodon/features/explore/components/card.jsx
@@ -25,7 +25,7 @@ export const Card = ({ id, source }) => {
   const dispatch = useDispatch();
 
   const handleDismiss = useCallback(() => {
-    dispatch(dismissSuggestion(id));
+    dispatch(dismissSuggestion({ accountId: id }));
   }, [id, dispatch]);
 
   let label;
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.jsx b/app/javascript/mastodon/features/favourited_statuses/index.jsx
index f7d6d14178..9049a20f05 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.jsx
+++ b/app/javascript/mastodon/features/favourited_statuses/index.jsx
@@ -101,6 +101,7 @@ class Favourites extends ImmutablePureComponent {
           onLoadMore={this.handleLoadMore}
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
+          timelineId='favourites'
         />
 
         <Helmet>
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.tsx b/app/javascript/mastodon/features/favourited_statuses/index.tsx
new file mode 100644
index 0000000000..908a8ae4a1
--- /dev/null
+++ b/app/javascript/mastodon/features/favourited_statuses/index.tsx
@@ -0,0 +1,116 @@
+import { useEffect, useRef, useCallback } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import {
+  fetchFavouritedStatuses,
+  expandFavouritedStatuses,
+} from 'mastodon/actions/favourites';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusList from 'mastodon/components/status_list';
+import { getStatusList } from 'mastodon/selectors';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const messages = defineMessages({
+  heading: { id: 'column.favourites', defaultMessage: 'Favorites' },
+});
+
+const Favourites: React.FC<{ columnId: string; multiColumn: boolean }> = ({
+  columnId,
+  multiColumn,
+}) => {
+  const dispatch = useAppDispatch();
+  const intl = useIntl();
+  const columnRef = useRef<ColumnRef>(null);
+  const statusIds = useAppSelector((state) =>
+    getStatusList(state, 'favourites'),
+  );
+  const isLoading = useAppSelector(
+    (state) =>
+      state.status_lists.getIn(['favourites', 'isLoading'], true) as boolean,
+  );
+  const hasMore = useAppSelector(
+    (state) => !!state.status_lists.getIn(['favourites', 'next']),
+  );
+
+  useEffect(() => {
+    dispatch(fetchFavouritedStatuses());
+  }, [dispatch]);
+
+  const handlePin = useCallback(() => {
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('FAVOURITES', {}));
+    }
+  }, [dispatch, columnId]);
+
+  const handleMove = useCallback(
+    (dir: number) => {
+      dispatch(moveColumn(columnId, dir));
+    },
+    [dispatch, columnId],
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const handleLoadMore = useCallback(() => {
+    dispatch(expandFavouritedStatuses());
+  }, [dispatch]);
+
+  const pinned = !!columnId;
+
+  const emptyMessage = (
+    <FormattedMessage
+      id='empty_column.favourited_statuses'
+      defaultMessage="You don't have any favorite posts yet. When you favorite one, it will show up here."
+    />
+  );
+
+  return (
+    <Column
+      bindToDocument={!multiColumn}
+      ref={columnRef}
+      label={intl.formatMessage(messages.heading)}
+    >
+      <ColumnHeader
+        icon='star'
+        iconComponent={StarIcon}
+        title={intl.formatMessage(messages.heading)}
+        onPin={handlePin}
+        onMove={handleMove}
+        onClick={handleHeaderClick}
+        pinned={pinned}
+        multiColumn={multiColumn}
+      />
+
+      <StatusList
+        trackScroll={!pinned}
+        statusIds={statusIds}
+        scrollKey={`favourited_statuses-${columnId}`}
+        hasMore={hasMore}
+        isLoading={isLoading}
+        onLoadMore={handleLoadMore}
+        emptyMessage={emptyMessage}
+        bindToDocument={!multiColumn}
+        timelineId='favourites'
+      />
+
+      <Helmet>
+        <title>{intl.formatMessage(messages.heading)}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default Favourites;
diff --git a/app/javascript/mastodon/features/followed_tags/index.jsx b/app/javascript/mastodon/features/followed_tags/index.jsx
deleted file mode 100644
index 21248e6de9..0000000000
--- a/app/javascript/mastodon/features/followed_tags/index.jsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import { Helmet } from 'react-helmet';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { debounce } from 'lodash';
-
-import TagIcon from '@/material-icons/400-24px/tag.svg?react';
-import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
-import ColumnHeader from 'mastodon/components/column_header';
-import { Hashtag } from 'mastodon/components/hashtag';
-import ScrollableList from 'mastodon/components/scrollable_list';
-import Column from 'mastodon/features/ui/components/column';
-
-const messages = defineMessages({
-  heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
-});
-
-const mapStateToProps = state => ({
-  hashtags: state.getIn(['followed_tags', 'items']),
-  isLoading: state.getIn(['followed_tags', 'isLoading'], true),
-  hasMore: !!state.getIn(['followed_tags', 'next']),
-});
-
-class FollowedTags extends ImmutablePureComponent {
-
-  static propTypes = {
-    params: PropTypes.object.isRequired,
-    dispatch: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-    hashtags: ImmutablePropTypes.list,
-    isLoading: PropTypes.bool,
-    hasMore: PropTypes.bool,
-    multiColumn: PropTypes.bool,
-  };
-
-  componentDidMount() {
-    this.props.dispatch(fetchFollowedHashtags());
-  }
-
-  handleLoadMore = debounce(() => {
-    this.props.dispatch(expandFollowedHashtags());
-  }, 300, { leading: true });
-
-  render () {
-    const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
-
-    const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
-
-    return (
-      <Column bindToDocument={!multiColumn}>
-        <ColumnHeader
-          icon='hashtag'
-          iconComponent={TagIcon}
-          title={intl.formatMessage(messages.heading)}
-          showBackButton
-          multiColumn={multiColumn}
-        />
-
-        <ScrollableList
-          scrollKey='followed_tags'
-          emptyMessage={emptyMessage}
-          hasMore={hasMore}
-          isLoading={isLoading}
-          onLoadMore={this.handleLoadMore}
-          bindToDocument={!multiColumn}
-        >
-          {hashtags.map((hashtag) => (
-            <Hashtag
-              key={hashtag.get('name')}
-              name={hashtag.get('name')}
-              to={`/tags/${hashtag.get('name')}`}
-              withGraph={false}
-              // Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
-              people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
-              history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
-            />
-          ))}
-        </ScrollableList>
-
-        <Helmet>
-          <meta name='robots' content='noindex' />
-        </Helmet>
-      </Column>
-    );
-  }
-
-}
-
-export default connect(mapStateToProps)(injectIntl(FollowedTags));
diff --git a/app/javascript/mastodon/features/followed_tags/index.tsx b/app/javascript/mastodon/features/followed_tags/index.tsx
new file mode 100644
index 0000000000..21d63a6fec
--- /dev/null
+++ b/app/javascript/mastodon/features/followed_tags/index.tsx
@@ -0,0 +1,161 @@
+import { useEffect, useState, useCallback, useRef } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { isFulfilled } from '@reduxjs/toolkit';
+
+import TagIcon from '@/material-icons/400-24px/tag.svg?react';
+import { unfollowHashtag } from 'mastodon/actions/tags_typed';
+import { apiGetFollowedTags } from 'mastodon/api/tags';
+import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
+import { Button } from 'mastodon/components/button';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import { Hashtag } from 'mastodon/components/hashtag';
+import ScrollableList from 'mastodon/components/scrollable_list';
+import { useAppDispatch } from 'mastodon/store';
+
+const messages = defineMessages({
+  heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
+});
+
+const FollowedTag: React.FC<{
+  tag: ApiHashtagJSON;
+  onUnfollow: (arg0: string) => void;
+}> = ({ tag, onUnfollow }) => {
+  const dispatch = useAppDispatch();
+  const tagId = tag.name;
+
+  const handleClick = useCallback(() => {
+    void dispatch(unfollowHashtag({ tagId })).then((result) => {
+      if (isFulfilled(result)) {
+        onUnfollow(tagId);
+      }
+
+      return '';
+    });
+  }, [dispatch, onUnfollow, tagId]);
+
+  const people =
+    parseInt(tag.history[0].accounts) +
+    parseInt(tag.history[1]?.accounts ?? '');
+
+  return (
+    <Hashtag
+      name={tag.name}
+      to={`/tags/${tag.name}`}
+      withGraph={false}
+      people={people}
+    >
+      <Button onClick={handleClick}>
+        <FormattedMessage id='account.unfollow' defaultMessage='Unfollow' />
+      </Button>
+    </Hashtag>
+  );
+};
+
+const FollowedTags: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
+  const intl = useIntl();
+  const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [next, setNext] = useState<string | undefined>();
+  const hasMore = !!next;
+  const columnRef = useRef<ColumnRef>(null);
+
+  useEffect(() => {
+    setLoading(true);
+
+    void apiGetFollowedTags()
+      .then(({ tags, links }) => {
+        const next = links.refs.find((link) => link.rel === 'next');
+
+        setTags(tags);
+        setLoading(false);
+        setNext(next?.uri);
+
+        return '';
+      })
+      .catch(() => {
+        setLoading(false);
+      });
+  }, [setTags, setLoading, setNext]);
+
+  const handleLoadMore = useCallback(() => {
+    setLoading(true);
+
+    void apiGetFollowedTags(next)
+      .then(({ tags, links }) => {
+        const next = links.refs.find((link) => link.rel === 'next');
+
+        setLoading(false);
+        setTags((previousTags) => [...previousTags, ...tags]);
+        setNext(next?.uri);
+
+        return '';
+      })
+      .catch(() => {
+        setLoading(false);
+      });
+  }, [setTags, setLoading, setNext, next]);
+
+  const handleUnfollow = useCallback(
+    (tagId: string) => {
+      setTags((tags) => tags.filter((tag) => tag.name !== tagId));
+    },
+    [setTags],
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const emptyMessage = (
+    <FormattedMessage
+      id='empty_column.followed_tags'
+      defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.'
+    />
+  );
+
+  return (
+    <Column
+      bindToDocument={!multiColumn}
+      ref={columnRef}
+      label={intl.formatMessage(messages.heading)}
+    >
+      <ColumnHeader
+        icon='hashtag'
+        iconComponent={TagIcon}
+        title={intl.formatMessage(messages.heading)}
+        onClick={handleHeaderClick}
+        multiColumn={multiColumn}
+        showBackButton
+      />
+
+      <ScrollableList
+        scrollKey='followed_tags'
+        emptyMessage={emptyMessage}
+        hasMore={hasMore}
+        isLoading={loading}
+        showLoading={loading && tags.length === 0}
+        onLoadMore={handleLoadMore}
+        trackScroll={!multiColumn}
+        bindToDocument={!multiColumn}
+      >
+        {tags.map((tag) => (
+          <FollowedTag key={tag.name} tag={tag} onUnfollow={handleUnfollow} />
+        ))}
+      </ScrollableList>
+
+      <Helmet>
+        <title>{intl.formatMessage(messages.heading)}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default FollowedTags;
diff --git a/app/javascript/mastodon/features/followers/index.jsx b/app/javascript/mastodon/features/followers/index.jsx
index 9cda4a7a4b..d72ee3d96e 100644
--- a/app/javascript/mastodon/features/followers/index.jsx
+++ b/app/javascript/mastodon/features/followers/index.jsx
@@ -10,10 +10,11 @@ import { debounce } from 'lodash';
 
 import { Account } from 'mastodon/components/account';
 import { TimelineHint } from 'mastodon/components/timeline_hint';
+import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
 import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 import { isHideItem, me } from 'mastodon/initial_state';
 import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
-import { getAccountHidden } from 'mastodon/selectors';
+import { getAccountHidden } from 'mastodon/selectors/accounts';
 import { useAppSelector } from 'mastodon/store';
 
 import {
@@ -26,7 +27,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
 import { LoadingIndicator } from '../../components/loading_indicator';
 import ScrollableList from '../../components/scrollable_list';
 import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
-import HeaderContainer from '../account_timeline/containers/header_container';
 import Column from '../ui/components/column';
 
 const mapStateToProps = (state, { params: { acct, id } }) => {
@@ -172,7 +172,7 @@ class Followers extends ImmutablePureComponent {
           hasMore={!forceEmptyState && hasMore}
           isLoading={isLoading}
           onLoadMore={this.handleLoadMore}
-          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
+          prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
           alwaysPrepend
           append={remoteMessage}
           emptyMessage={emptyMessage}
diff --git a/app/javascript/mastodon/features/following/index.jsx b/app/javascript/mastodon/features/following/index.jsx
index ceec01eefb..fc2cea763f 100644
--- a/app/javascript/mastodon/features/following/index.jsx
+++ b/app/javascript/mastodon/features/following/index.jsx
@@ -10,10 +10,11 @@ import { debounce } from 'lodash';
 
 import { Account } from 'mastodon/components/account';
 import { TimelineHint } from 'mastodon/components/timeline_hint';
+import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
 import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 import { isHideItem, me } from 'mastodon/initial_state';
 import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
-import { getAccountHidden } from 'mastodon/selectors';
+import { getAccountHidden } from 'mastodon/selectors/accounts';
 import { useAppSelector } from 'mastodon/store';
 
 import {
@@ -26,7 +27,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
 import { LoadingIndicator } from '../../components/loading_indicator';
 import ScrollableList from '../../components/scrollable_list';
 import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
-import HeaderContainer from '../account_timeline/containers/header_container';
 import Column from '../ui/components/column';
 
 const mapStateToProps = (state, { params: { acct, id } }) => {
@@ -170,7 +170,7 @@ class Following extends ImmutablePureComponent {
           hasMore={!forceEmptyState && hasMore}
           isLoading={isLoading}
           onLoadMore={this.handleLoadMore}
-          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
+          prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
           alwaysPrepend
           append={remoteMessage}
           emptyMessage={emptyMessage}
diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.jsx b/app/javascript/mastodon/features/getting_started/components/announcements.jsx
index ad66d2e5fa..f5f593860f 100644
--- a/app/javascript/mastodon/features/getting_started/components/announcements.jsx
+++ b/app/javascript/mastodon/features/getting_started/components/announcements.jsx
@@ -1,5 +1,5 @@
 import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
+import { PureComponent, useCallback, useMemo } from 'react';
 
 import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
 
@@ -9,8 +9,7 @@ import { withRouter } from 'react-router-dom';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
-import TransitionMotion from 'react-motion/lib/TransitionMotion';
-import spring from 'react-motion/lib/spring';
+import { animated, useTransition } from '@react-spring/web';
 import ReactSwipeableViews from 'react-swipeable-views';
 
 import elephantUIPlane from '@/images/elephant_ui_plane.svg';
@@ -239,72 +238,76 @@ class Reaction extends ImmutablePureComponent {
     }
 
     return (
-      <button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
+      <animated.button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
         <span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span>
         <span className='reactions-bar__item__count'><AnimatedNumber value={reaction.get('count')} /></span>
-      </button>
+      </animated.button>
     );
   }
 
 }
 
-class ReactionsBar extends ImmutablePureComponent {
+const ReactionsBar = ({
+  announcementId,
+  reactions,
+  emojiMap,
+  addReaction,
+  removeReaction,
+}) => {
+  const visibleReactions = useMemo(() => reactions.filter(x => x.get('count') > 0).toArray(), [reactions]);
 
-  static propTypes = {
-    announcementId: PropTypes.string.isRequired,
-    reactions: ImmutablePropTypes.list.isRequired,
-    addReaction: PropTypes.func.isRequired,
-    removeReaction: PropTypes.func.isRequired,
-    emojiMap: ImmutablePropTypes.map.isRequired,
-  };
+  const handleEmojiPick = useCallback((emoji) => {
+    addReaction(announcementId, emoji.native.replaceAll(/:/g, ''));
+  }, [addReaction, announcementId]);
 
-  handleEmojiPick = data => {
-    const { addReaction, announcementId } = this.props;
-    addReaction(announcementId, data.native.replace(/:/g, ''));
-  };
+  const transitions = useTransition(visibleReactions, {
+    from: {
+      scale: 0,
+    },
+    enter: {
+      scale: 1,
+    },
+    leave: {
+      scale: 0,
+    },
+    immediate: reduceMotion,
+    keys: visibleReactions.map(x => x.get('name')),
+  });
 
-  willEnter () {
-    return { scale: reduceMotion ? 1 : 0 };
-  }
+  return (
+    <div
+      className={classNames('reactions-bar', {
+        'reactions-bar--empty': visibleReactions.length === 0
+      })}
+    >
+      {transitions(({ scale }, reaction) => (
+        <Reaction
+          key={reaction.get('name')}
+          reaction={reaction}
+          style={{ transform: scale.to((s) => `scale(${s})`) }}
+          addReaction={addReaction}
+          removeReaction={removeReaction}
+          announcementId={announcementId}
+          emojiMap={emojiMap}
+        />
+      ))}
 
-  willLeave () {
-    return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
-  }
-
-  render () {
-    const { reactions } = this.props;
-    const visibleReactions = reactions.filter(x => x.get('count') > 0);
-
-    const styles = visibleReactions.map(reaction => ({
-      key: reaction.get('name'),
-      data: reaction,
-      style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
-    })).toArray();
-
-    return (
-      <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
-        {items => (
-          <div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
-            {items.map(({ key, data, style }) => (
-              <Reaction
-                key={key}
-                reaction={data}
-                style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
-                announcementId={this.props.announcementId}
-                addReaction={this.props.addReaction}
-                removeReaction={this.props.removeReaction}
-                emojiMap={this.props.emojiMap}
-              />
-            ))}
-
-            {visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={<Icon id='plus' icon={AddIcon} />} />}
-          </div>
-        )}
-      </TransitionMotion>
-    );
-  }
-
-}
+      {visibleReactions.length < 8 && (
+        <EmojiPickerDropdown
+          onPickEmoji={handleEmojiPick}
+          button={<Icon id='plus' icon={AddIcon} />}
+        />
+      )}
+    </div>
+  );
+};
+ReactionsBar.propTypes = {
+  announcementId: PropTypes.string.isRequired,
+  reactions: ImmutablePropTypes.list.isRequired,
+  addReaction: PropTypes.func.isRequired,
+  removeReaction: PropTypes.func.isRequired,
+  emojiMap: ImmutablePropTypes.map.isRequired,
+};
 
 class Announcement extends ImmutablePureComponent {
 
diff --git a/app/javascript/mastodon/features/getting_started/index.jsx b/app/javascript/mastodon/features/getting_started/index.jsx
index ff3166da35..48ce17881a 100644
--- a/app/javascript/mastodon/features/getting_started/index.jsx
+++ b/app/javascript/mastodon/features/getting_started/index.jsx
@@ -14,6 +14,8 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
 import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
 import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
 import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react';
+import HashtagIcon from '@/material-icons/400-24px/tag.svg?react';
+import Directory from '@/material-icons/400-24px/group.svg?react';
 import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
 import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
 import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
@@ -42,6 +44,8 @@ const messages = defineMessages({
   home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
   public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
+  followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed Hashtags' },
+  directory: { id: 'navigation_bar.directory', defaultMessage: 'Profile directory' },
   settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
   community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   deep_timeline: { id: 'navigation_bar.deep_timeline', defaultMessage: 'Deep timeline' },
@@ -144,6 +148,7 @@ class GettingStarted extends ImmutablePureComponent {
         <ColumnLink key='direct' icon='at' iconComponent={AlternateEmailIcon} text={intl.formatMessage(messages.direct)} to='/conversations' />,
         <ColumnLink key='bookmark' icon='bookmarks' iconComponent={BookmarksIcon} text={intl.formatMessage(messages.bookmarks)} to='/bookmark_categories' />,
         <ColumnLink key='favourites' icon='star' iconComponent={StarIcon} text={intl.formatMessage(messages.favourites)} to='/favourites' />,
+        <ColumnLink key='followed_tags' icon='tag' iconComponent={followed_tagsIcon} text={intl.formatMessage(messages.followed_tags)} to='/followed_tags' />,
         <ColumnLink key='lists' icon='list-ul' iconComponent={ListAltIcon} text={intl.formatMessage(messages.lists)} to='/lists' />,
         <ColumnLink key='antennas' icon='wifi' iconComponent={AntennaIcon} text={intl.formatMessage(messages.antennas)} to='/antennas' />,
         <ColumnLink key='circles' icon='user-circle' iconComponent={CirclesIcon} text={intl.formatMessage(messages.circles)} to='/circles' />,
diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx
index 7372fe5284..b7c1ea02d9 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx
@@ -12,8 +12,8 @@ import {
 } from 'mastodon/actions/tags_typed';
 import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
 import { Button } from 'mastodon/components/button';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { ShortNumber } from 'mastodon/components/short_number';
-import DropdownMenu from 'mastodon/containers/dropdown_menu_container';
 import { useIdentity } from 'mastodon/identity_context';
 import { PERMISSION_MANAGE_TAXONOMIES } from 'mastodon/permissions';
 import { useAppDispatch } from 'mastodon/store';
@@ -153,13 +153,11 @@ export const HashtagHeader: React.FC<{
 
         <div className='hashtag-header__header__buttons'>
           {menu.length > 0 && (
-            <DropdownMenu
+            <Dropdown
               disabled={menu.length === 0}
               items={menu}
               icon='ellipsis-v'
               iconComponent={MoreHorizIcon}
-              size={24}
-              direction='right'
             />
           )}
 
diff --git a/app/javascript/mastodon/features/home_timeline/components/column_settings.tsx b/app/javascript/mastodon/features/home_timeline/components/column_settings.tsx
index 3f0525fe57..cb03d38847 100644
--- a/app/javascript/mastodon/features/home_timeline/components/column_settings.tsx
+++ b/app/javascript/mastodon/features/home_timeline/components/column_settings.tsx
@@ -17,7 +17,7 @@ export const ColumnSettings: React.FC = () => {
 
   const dispatch = useAppDispatch();
   const onChange = useCallback(
-    (key: string, checked: boolean) => {
+    (key: string[], checked: boolean) => {
       dispatch(changeSetting(['home', ...key], checked));
     },
     [dispatch],
diff --git a/app/javascript/mastodon/features/lists/index.tsx b/app/javascript/mastodon/features/lists/index.tsx
index ad172908bd..e8f55bc0ad 100644
--- a/app/javascript/mastodon/features/lists/index.tsx
+++ b/app/javascript/mastodon/features/lists/index.tsx
@@ -13,9 +13,9 @@ import { fetchLists } from 'mastodon/actions/lists';
 import { openModal } from 'mastodon/actions/modal';
 import { Column } from 'mastodon/components/column';
 import { ColumnHeader } from 'mastodon/components/column_header';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { Icon } from 'mastodon/components/icon';
 import ScrollableList from 'mastodon/components/scrollable_list';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
 import { getOrderedLists } from 'mastodon/selectors/lists';
 import { useAppSelector, useAppDispatch } from 'mastodon/store';
 
@@ -72,12 +72,11 @@ const ListItem: React.FC<{
         </span>
       </Link>
 
-      <DropdownMenuContainer
+      <Dropdown
         scrollKey='lists'
         items={menu}
-        icons='ellipsis-h'
+        icon='ellipsis-h'
         iconComponent={MoreHorizIcon}
-        direction='right'
         title={intl.formatMessage(messages.more)}
       />
     </div>
diff --git a/app/javascript/mastodon/features/notifications/components/notification_request.jsx b/app/javascript/mastodon/features/notifications/components/notification_request.jsx
index 626929ae50..381bb1153f 100644
--- a/app/javascript/mastodon/features/notifications/components/notification_request.jsx
+++ b/app/javascript/mastodon/features/notifications/components/notification_request.jsx
@@ -17,7 +17,7 @@ import { initReport } from 'mastodon/actions/reports';
 import { Avatar } from 'mastodon/components/avatar';
 import { CheckBox } from 'mastodon/components/check_box';
 import { IconButton } from 'mastodon/components/icon_button';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { makeGetAccount } from 'mastodon/selectors';
 import { toCappedNumber } from 'mastodon/utils/numbers';
 
@@ -105,11 +105,10 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked
 
       <div className='notification-request__actions'>
         <IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
-        <DropdownMenuContainer
+        <Dropdown
           items={menu}
-          icons='ellipsis-h'
+          icon='ellipsis-h'
           iconComponent={MoreHorizIcon}
-          direction='right'
           title={intl.formatMessage(messages.more)}
         />
       </div>
diff --git a/app/javascript/mastodon/features/notifications/requests.jsx b/app/javascript/mastodon/features/notifications/requests.jsx
index ccaed312b4..b2bdd0ec77 100644
--- a/app/javascript/mastodon/features/notifications/requests.jsx
+++ b/app/javascript/mastodon/features/notifications/requests.jsx
@@ -23,7 +23,7 @@ import Column from 'mastodon/components/column';
 import ColumnHeader from 'mastodon/components/column_header';
 import { Icon } from 'mastodon/components/icon';
 import ScrollableList from 'mastodon/components/scrollable_list';
-import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 
 import { NotificationRequest } from './components/notification_request';
 import { PolicyControls } from './components/policy_controls';
@@ -126,7 +126,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
       <div className='column-header__select-row__checkbox'>
         <CheckBox checked={selectAllChecked} indeterminate={selectedCount > 0 && !selectAllChecked} onChange={handleSelectAll} />
       </div>
-      <DropdownMenuContainer
+      <Dropdown
         items={menu}
         icons='ellipsis-h'
         iconComponent={MoreHorizIcon}
@@ -139,7 +139,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
           </span>
           <Icon id='down' icon={ArrowDropDownIcon} />
         </button>
-      </DropdownMenuContainer>
+      </Dropdown>
       <div className='column-header__select-row__mode-button'>
         <button className='text-btn' tabIndex={0} onClick={handleToggleSelectionMode}>
           {selectionMode ? (
diff --git a/app/javascript/mastodon/features/picture_in_picture/index.tsx b/app/javascript/mastodon/features/picture_in_picture/index.tsx
index 51b72f9725..9bae1b5545 100644
--- a/app/javascript/mastodon/features/picture_in_picture/index.tsx
+++ b/app/javascript/mastodon/features/picture_in_picture/index.tsx
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
 
 import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
 import Audio from 'mastodon/features/audio';
-import Video from 'mastodon/features/video';
+import { Video } from 'mastodon/features/video';
 import { useAppDispatch, useAppSelector } from 'mastodon/store/typed_functions';
 
 import Footer from './components/footer';
@@ -35,6 +35,10 @@ export const PictureInPicture: React.FC = () => {
     accentColor,
   } = pipState;
 
+  if (!src) {
+    return null;
+  }
+
   let player;
 
   switch (type) {
@@ -42,11 +46,10 @@ export const PictureInPicture: React.FC = () => {
       player = (
         <Video
           src={src}
-          currentTime={currentTime}
-          volume={volume}
-          muted={muted}
-          autoPlay
-          inline
+          startTime={currentTime}
+          startVolume={volume}
+          startMuted={muted}
+          startPlaying
           alwaysVisible
         />
       );
diff --git a/app/javascript/mastodon/features/privacy_policy/index.tsx b/app/javascript/mastodon/features/privacy_policy/index.tsx
index f0309c2712..cd6f9f3b2b 100644
--- a/app/javascript/mastodon/features/privacy_policy/index.tsx
+++ b/app/javascript/mastodon/features/privacy_policy/index.tsx
@@ -1,17 +1,13 @@
 import { useState, useEffect } from 'react';
 
-import {
-  FormattedMessage,
-  FormattedDate,
-  useIntl,
-  defineMessages,
-} from 'react-intl';
+import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
 
 import { apiGetPrivacyPolicy } from 'mastodon/api/instance';
 import type { ApiPrivacyPolicyJSON } from 'mastodon/api_types/instance';
 import { Column } from 'mastodon/components/column';
+import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
 import { Skeleton } from 'mastodon/components/skeleton';
 
 const messages = defineMessages({
@@ -58,7 +54,7 @@ const PrivacyPolicy: React.FC<{
                 date: loading ? (
                   <Skeleton width='10ch' />
                 ) : (
-                  <FormattedDate
+                  <FormattedDateWrapper
                     value={response?.updated_at}
                     year='numeric'
                     month='short'
diff --git a/app/javascript/mastodon/features/search/index.tsx b/app/javascript/mastodon/features/search/index.tsx
index d7fd3f9663..8ef64413f9 100644
--- a/app/javascript/mastodon/features/search/index.tsx
+++ b/app/javascript/mastodon/features/search/index.tsx
@@ -4,7 +4,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
 
-import { useSearchParam } from '@/hooks/useSearchParam';
 import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
 import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
 import SearchIcon from '@/material-icons/400-24px/search.svg?react';
@@ -20,6 +19,7 @@ import { Icon } from 'mastodon/components/icon';
 import ScrollableList from 'mastodon/components/scrollable_list';
 import Status from 'mastodon/containers/status_container';
 import { Search } from 'mastodon/features/compose/components/search';
+import { useSearchParam } from 'mastodon/hooks/useSearchParam';
 import type { Hashtag as HashtagType } from 'mastodon/models/tags';
 import { useAppDispatch, useAppSelector } from 'mastodon/store';
 
diff --git a/app/javascript/mastodon/features/standalone/compose/index.jsx b/app/javascript/mastodon/features/standalone/compose/index.jsx
index 241d9aadfd..3aff78ffee 100644
--- a/app/javascript/mastodon/features/standalone/compose/index.jsx
+++ b/app/javascript/mastodon/features/standalone/compose/index.jsx
@@ -1,12 +1,12 @@
+import { AlertsController } from 'mastodon/components/alerts_controller';
 import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
 import LoadingBarContainer from 'mastodon/features/ui/containers/loading_bar_container';
 import ModalContainer from 'mastodon/features/ui/containers/modal_container';
-import NotificationsContainer from 'mastodon/features/ui/containers/notifications_container';
 
 const Compose = () => (
   <>
     <ComposeFormContainer autoFocus withoutNavigation />
-    <NotificationsContainer />
+    <AlertsController />
     <ModalContainer />
     <LoadingBarContainer className='loading-bar' />
   </>
diff --git a/app/javascript/mastodon/features/standalone/status/index.tsx b/app/javascript/mastodon/features/standalone/status/index.tsx
index 3b68c5c176..8d1b831467 100644
--- a/app/javascript/mastodon/features/standalone/status/index.tsx
+++ b/app/javascript/mastodon/features/standalone/status/index.tsx
@@ -6,11 +6,11 @@ import { useEffect, useCallback } from 'react';
 
 import { Provider } from 'react-redux';
 
-import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
 import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
 import { hydrateStore } from 'mastodon/actions/store';
 import { Router } from 'mastodon/components/router';
 import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
+import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
 import initialState from 'mastodon/initial_state';
 import { IntlProvider } from 'mastodon/locales';
 import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx
index 3e686d0d94..f56a616b9d 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.jsx
+++ b/app/javascript/mastodon/features/status/components/action_bar.jsx
@@ -27,7 +27,7 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
 
 
 import { IconButton } from '../../../components/icon_button';
-import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
 import { enableEmojiReaction , bookmarkCategoryNeeded, me, isHideItem, boostMenu, boostModal } from '../../../initial_state';
 import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
 
@@ -416,7 +416,7 @@ class ActionBar extends PureComponent {
           <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
         ) : (
           <div className='detailed-status__button'>
-            <DropdownMenuContainer
+            <Dropdown
               className={classNames({ reblogPrivate })}
               icon='retweet'
               iconComponent={reblogIconComponent}
@@ -434,7 +434,7 @@ class ActionBar extends PureComponent {
         {emojiPickerDropdown}
 
         <div className='detailed-status__action-bar-dropdown'>
-          <DropdownMenuContainer icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
+          <Dropdown icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx
index 1540f50065..e0a68ad263 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.tsx
+++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx
@@ -6,7 +6,7 @@
 import type { CSSProperties } from 'react';
 import { useState, useRef, useCallback } from 'react';
 
-import { FormattedDate, FormattedMessage } from 'react-intl';
+import { FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
 import { Link } from 'react-router-dom';
@@ -14,7 +14,9 @@ import { Link } from 'react-router-dom';
 import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 import { AnimatedNumber } from 'mastodon/components/animated_number';
 import { ContentWarning } from 'mastodon/components/content_warning';
-import EditedTimestamp from 'mastodon/components/edited_timestamp';
+import { EditedTimestamp } from 'mastodon/components/edited_timestamp';
+import { FilterWarning } from 'mastodon/components/filter_warning';
+import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
 import type { StatusLike } from 'mastodon/components/hashtag_bar';
 import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
 import { Icon } from 'mastodon/components/icon';
@@ -22,6 +24,7 @@ import { IconLogo } from 'mastodon/components/logo';
 import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
 import { SearchabilityIcon } from 'mastodon/components/searchability_icon';
 import { VisibilityIcon } from 'mastodon/components/visibility_icon';
+import { Video } from 'mastodon/features/video';
 import { enableEmojiReaction, isHideItem } from 'mastodon/initial_state';
 
 import { Avatar } from '../../../components/avatar';
@@ -32,7 +35,6 @@ import StatusEmojiReactionsBar from '../../../components/status_emoji_reactions_
 import CompactedStatusContainer from '../../../containers/compacted_status_container';
 import Audio from '../../audio';
 import scheduleIdleTask from '../../ui/util/schedule_idle_task';
-import Video from '../../video';
 
 import Card from './card';
 
@@ -40,7 +42,6 @@ interface VideoModalOptions {
   startTime: number;
   autoPlay?: boolean;
   defaultVolume: number;
-  componentIndex: number;
 }
 
 export const DetailedStatus: React.FC<{
@@ -80,6 +81,7 @@ export const DetailedStatus: React.FC<{
 }) => {
   const properStatus = status?.get('reblog') ?? status;
   const [height, setHeight] = useState(0);
+  const [showDespiteFilter, setShowDespiteFilter] = useState(false);
   const nodeRef = useRef<HTMLDivElement>();
 
   const handleOpenVideo = useCallback(
@@ -92,6 +94,10 @@ export const DetailedStatus: React.FC<{
     [onOpenVideo, status],
   );
 
+  const handleFilterToggle = useCallback(() => {
+    setShowDespiteFilter(!showDespiteFilter);
+  }, [showDespiteFilter, setShowDespiteFilter]);
+
   const handleExpandedToggle = useCallback(() => {
     if (onToggleHidden) onToggleHidden(status);
   }, [onToggleHidden, status]);
@@ -180,6 +186,7 @@ export const DetailedStatus: React.FC<{
           onOpenMedia={onOpenMedia}
           visible={showMedia}
           onToggleVisibility={onToggleMediaVisibility}
+          matchedFilters={status.get('matched_media_filters')}
         />
       );
     } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
@@ -206,6 +213,7 @@ export const DetailedStatus: React.FC<{
           blurhash={attachment.get('blurhash')}
           height={150}
           onToggleVisibility={onToggleMediaVisibility}
+          matchedFilters={status.get('matched_media_filters')}
         />
       );
     } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
@@ -223,12 +231,11 @@ export const DetailedStatus: React.FC<{
           src={attachment.get('url')}
           alt={description}
           lang={language}
-          width={300}
-          height={150}
           onOpenVideo={handleOpenVideo}
           sensitive={status.get('sensitive')}
           visible={showMedia}
           onToggleVisibility={onToggleMediaVisibility}
+          matchedFilters={status.get('matched_media_filters')}
         />
       );
     }
@@ -237,7 +244,7 @@ export const DetailedStatus: React.FC<{
       <Card
         sensitive={status.get('sensitive') && !status.get('spoiler_text')}
         onOpenMedia={onOpenMedia}
-        card={status.get('card', null)}
+        card={status.get('card')}
       />
     );
   }
@@ -369,8 +376,12 @@ export const DetailedStatus: React.FC<{
   const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
     status as StatusLike,
   );
+
+  const matchedFilters = status.get('matched_filters');
+
   const expanded =
-    !status.get('hidden') || status.get('spoiler_text').length === 0;
+    (!matchedFilters || showDespiteFilter) &&
+    (!status.get('hidden') || status.get('spoiler_text').length === 0);
 
   const quote = !muted && status.get('quote_id') && (
     <>
@@ -418,17 +429,26 @@ export const DetailedStatus: React.FC<{
           )}
         </Link>
 
-        {status.get('spoiler_text').length > 0 && (
-          <ContentWarning
-            text={
-              status.getIn(['translation', 'spoilerHtml']) ||
-              status.get('spoilerHtml')
-            }
-            expanded={expanded}
-            onClick={handleExpandedToggle}
+        {matchedFilters && (
+          <FilterWarning
+            title={matchedFilters.join(', ')}
+            expanded={showDespiteFilter}
+            onClick={handleFilterToggle}
           />
         )}
 
+        {status.get('spoiler_text').length > 0 &&
+          (!matchedFilters || showDespiteFilter) && (
+            <ContentWarning
+              text={
+                status.getIn(['translation', 'spoilerHtml']) ||
+                status.get('spoilerHtml')
+              }
+              expanded={expanded}
+              onClick={handleExpandedToggle}
+            />
+          )}
+
         {expanded && (
           <>
             <StatusContent
@@ -452,7 +472,7 @@ export const DetailedStatus: React.FC<{
               target='_blank'
               rel='noopener noreferrer'
             >
-              <FormattedDate
+              <FormattedDateWrapper
                 value={new Date(status.get('created_at') as string)}
                 year='numeric'
                 month='short'
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index 08a2a74e88..e9d0c9f8d6 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -147,7 +147,7 @@ const makeMapStateToProps = () => {
   });
 
   const mapStateToProps = (state, props) => {
-    const status = getStatus(state, { id: props.params.statusId });
+    const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
 
     let ancestorsIds   = ImmutableList();
     let descendantsIds = ImmutableList();
diff --git a/app/javascript/mastodon/features/terms_of_service/index.tsx b/app/javascript/mastodon/features/terms_of_service/index.tsx
index 05033bffec..8ef64fc515 100644
--- a/app/javascript/mastodon/features/terms_of_service/index.tsx
+++ b/app/javascript/mastodon/features/terms_of_service/index.tsx
@@ -8,26 +8,31 @@ import {
 } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
+import { Link, useParams } from 'react-router-dom';
 
 import { apiGetTermsOfService } from 'mastodon/api/instance';
 import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
 import { Column } from 'mastodon/components/column';
-import { Skeleton } from 'mastodon/components/skeleton';
 import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 
 const messages = defineMessages({
   title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
 });
 
+interface Params {
+  date?: string;
+}
+
 const TermsOfService: React.FC<{
   multiColumn: boolean;
 }> = ({ multiColumn }) => {
   const intl = useIntl();
+  const { date } = useParams<Params>();
   const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
   const [loading, setLoading] = useState(true);
 
   useEffect(() => {
-    apiGetTermsOfService()
+    apiGetTermsOfService(date)
       .then((data) => {
         setResponse(data);
         setLoading(false);
@@ -36,7 +41,7 @@ const TermsOfService: React.FC<{
       .catch(() => {
         setLoading(false);
       });
-  }, []);
+  }, [date]);
 
   if (!loading && !response) {
     return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
@@ -55,23 +60,60 @@ const TermsOfService: React.FC<{
               defaultMessage='Terms of Service'
             />
           </h3>
-          <p>
-            <FormattedMessage
-              id='privacy_policy.last_updated'
-              defaultMessage='Last updated {date}'
-              values={{
-                date: loading ? (
-                  <Skeleton width='10ch' />
-                ) : (
-                  <FormattedDate
-                    value={response?.updated_at}
-                    year='numeric'
-                    month='short'
-                    day='2-digit'
+          <p className='prose'>
+            {response?.effective ? (
+              <FormattedMessage
+                id='privacy_policy.last_updated'
+                defaultMessage='Last updated {date}'
+                values={{
+                  date: (
+                    <FormattedDate
+                      value={response.effective_date}
+                      year='numeric'
+                      month='short'
+                      day='2-digit'
+                    />
+                  ),
+                }}
+              />
+            ) : (
+              <FormattedMessage
+                id='terms_of_service.effective_as_of'
+                defaultMessage='Effective as of {date}'
+                values={{
+                  date: (
+                    <FormattedDate
+                      value={response?.effective_date}
+                      year='numeric'
+                      month='short'
+                      day='2-digit'
+                    />
+                  ),
+                }}
+              />
+            )}
+
+            {response?.succeeded_by && (
+              <>
+                {' · '}
+                <Link to={`/terms-of-service/${response.succeeded_by}`}>
+                  <FormattedMessage
+                    id='terms_of_service.upcoming_changes_on'
+                    defaultMessage='Upcoming changes on {date}'
+                    values={{
+                      date: (
+                        <FormattedDate
+                          value={response.succeeded_by}
+                          year='numeric'
+                          month='short'
+                          day='2-digit'
+                        />
+                      ),
+                    }}
                   />
-                ),
-              }}
-            />
+                </Link>
+              </>
+            )}
           </p>
         </div>
 
diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.tsx b/app/javascript/mastodon/features/ui/components/embed_modal.tsx
index 8f623e62b5..b78d5b64c4 100644
--- a/app/javascript/mastodon/features/ui/components/embed_modal.tsx
+++ b/app/javascript/mastodon/features/ui/components/embed_modal.tsx
@@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
           />
 
           <iframe
+            // eslint-disable-next-line @typescript-eslint/no-deprecated
             frameBorder='0'
             ref={iframeRef}
             sandbox='allow-scripts allow-same-origin'
diff --git a/app/javascript/mastodon/features/ui/components/hashtag_menu_controller.tsx b/app/javascript/mastodon/features/ui/components/hashtag_menu_controller.tsx
new file mode 100644
index 0000000000..6707b24672
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/hashtag_menu_controller.tsx
@@ -0,0 +1,157 @@
+import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
+
+import { useIntl, defineMessages } from 'react-intl';
+
+import { useLocation } from 'react-router-dom';
+
+import Overlay from 'react-overlays/Overlay';
+import type {
+  OffsetValue,
+  UsePopperOptions,
+} from 'react-overlays/esm/usePopper';
+
+import { DropdownMenu } from 'mastodon/components/dropdown_menu';
+import { useAppSelector } from 'mastodon/store';
+
+const messages = defineMessages({
+  browseHashtag: {
+    id: 'hashtag.browse',
+    defaultMessage: 'Browse posts in #{hashtag}',
+  },
+  browseHashtagFromAccount: {
+    id: 'hashtag.browse_from_account',
+    defaultMessage: 'Browse posts from @{name} in #{hashtag}',
+  },
+  muteHashtag: { id: 'hashtag.mute', defaultMessage: 'Mute #{hashtag}' },
+});
+
+const offset = [5, 5] as OffsetValue;
+const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
+
+const isHashtagLink = (
+  element: HTMLAnchorElement | null,
+): element is HTMLAnchorElement => {
+  if (!element) {
+    return false;
+  }
+
+  return element.matches('[data-menu-hashtag]');
+};
+
+interface TargetParams {
+  hashtag?: string;
+  accountId?: string;
+}
+
+export const HashtagMenuController: React.FC = () => {
+  const intl = useIntl();
+  const [open, setOpen] = useState(false);
+  const [{ accountId, hashtag }, setTargetParams] = useState<TargetParams>({});
+  const targetRef = useRef<HTMLAnchorElement | null>(null);
+  const location = useLocation();
+  const account = useAppSelector((state) =>
+    accountId ? state.accounts.get(accountId) : undefined,
+  );
+
+  useEffect(() => {
+    setOpen(false);
+    targetRef.current = null;
+  }, [setOpen, location]);
+
+  useEffect(() => {
+    const handleClick = (e: MouseEvent) => {
+      const target = (e.target as HTMLElement).closest('a');
+
+      if (e.button !== 0 || e.ctrlKey || e.metaKey) {
+        return;
+      }
+
+      if (!isHashtagLink(target)) {
+        return;
+      }
+
+      const hashtag = target.text.replace(/^#/, '');
+      const accountId = target.getAttribute('data-menu-hashtag');
+
+      if (!hashtag || !accountId) {
+        return;
+      }
+
+      e.preventDefault();
+      e.stopPropagation();
+      targetRef.current = target;
+      setOpen(true);
+      setTargetParams({ hashtag, accountId });
+    };
+
+    document.addEventListener('click', handleClick, { capture: true });
+
+    return () => {
+      document.removeEventListener('click', handleClick);
+    };
+  }, [setTargetParams, setOpen]);
+
+  const handleClose = useCallback(() => {
+    setOpen(false);
+    targetRef.current = null;
+  }, [setOpen]);
+
+  const menu = useMemo(
+    () => [
+      {
+        text: intl.formatMessage(messages.browseHashtag, {
+          hashtag,
+        }),
+        to: `/tags/${hashtag}`,
+      },
+      {
+        text: intl.formatMessage(messages.browseHashtagFromAccount, {
+          hashtag,
+          name: account?.username,
+        }),
+        to: `/@${account?.acct}/tagged/${hashtag}`,
+      },
+      null,
+      {
+        text: intl.formatMessage(messages.muteHashtag, {
+          hashtag,
+        }),
+        href: '/filters',
+        dangerous: true,
+      },
+    ],
+    [intl, hashtag, account],
+  );
+
+  if (!open) {
+    return null;
+  }
+
+  return (
+    <Overlay
+      show={open}
+      offset={offset}
+      placement='bottom'
+      flip
+      target={targetRef}
+      popperConfig={popperConfig}
+    >
+      {({ props, arrowProps, placement }) => (
+        <div {...props}>
+          <div className={`dropdown-animation dropdown-menu ${placement}`}>
+            <div
+              className={`dropdown-menu__arrow ${placement}`}
+              {...arrowProps}
+            />
+
+            <DropdownMenu
+              items={menu}
+              onClose={handleClose}
+              openedViaKeyboard={false}
+            />
+          </div>
+        </div>
+      )}
+    </Overlay>
+  );
+};
diff --git a/app/javascript/mastodon/features/ui/components/image_loader.jsx b/app/javascript/mastodon/features/ui/components/image_loader.jsx
deleted file mode 100644
index b1417deda7..0000000000
--- a/app/javascript/mastodon/features/ui/components/image_loader.jsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import classNames from 'classnames';
-
-import { LoadingBar } from 'react-redux-loading-bar';
-
-import ZoomableImage from './zoomable_image';
-
-export default class ImageLoader extends PureComponent {
-
-  static propTypes = {
-    alt: PropTypes.string,
-    lang: PropTypes.string,
-    src: PropTypes.string.isRequired,
-    previewSrc: PropTypes.string,
-    width: PropTypes.number,
-    height: PropTypes.number,
-    onClick: PropTypes.func,
-    zoomedIn: PropTypes.bool,
-  };
-
-  static defaultProps = {
-    alt: '',
-    lang: '',
-    width: null,
-    height: null,
-  };
-
-  state = {
-    loading: true,
-    error: false,
-    width: null,
-  };
-
-  removers = [];
-  canvas = null;
-
-  get canvasContext() {
-    if (!this.canvas) {
-      return null;
-    }
-    this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
-    return this._canvasContext;
-  }
-
-  componentDidMount () {
-    this.loadImage(this.props);
-  }
-
-  UNSAFE_componentWillReceiveProps (nextProps) {
-    if (this.props.src !== nextProps.src) {
-      this.loadImage(nextProps);
-    }
-  }
-
-  componentWillUnmount () {
-    this.removeEventListeners();
-  }
-
-  loadImage (props) {
-    this.removeEventListeners();
-    this.setState({ loading: true, error: false });
-    Promise.all([
-      props.previewSrc && this.loadPreviewCanvas(props),
-      this.hasSize() && this.loadOriginalImage(props),
-    ].filter(Boolean))
-      .then(() => {
-        this.setState({ loading: false, error: false });
-        this.clearPreviewCanvas();
-      })
-      .catch(() => this.setState({ loading: false, error: true }));
-  }
-
-  loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => {
-    const image = new Image();
-    const removeEventListeners = () => {
-      image.removeEventListener('error', handleError);
-      image.removeEventListener('load', handleLoad);
-    };
-    const handleError = () => {
-      removeEventListeners();
-      reject();
-    };
-    const handleLoad = () => {
-      removeEventListeners();
-      this.canvasContext.drawImage(image, 0, 0, width, height);
-      resolve();
-    };
-    image.addEventListener('error', handleError);
-    image.addEventListener('load', handleLoad);
-    image.src = previewSrc;
-    this.removers.push(removeEventListeners);
-  });
-
-  clearPreviewCanvas () {
-    const { width, height } = this.canvas;
-    this.canvasContext.clearRect(0, 0, width, height);
-  }
-
-  loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
-    const image = new Image();
-    const removeEventListeners = () => {
-      image.removeEventListener('error', handleError);
-      image.removeEventListener('load', handleLoad);
-    };
-    const handleError = () => {
-      removeEventListeners();
-      reject();
-    };
-    const handleLoad = () => {
-      removeEventListeners();
-      resolve();
-    };
-    image.addEventListener('error', handleError);
-    image.addEventListener('load', handleLoad);
-    image.src = src;
-    this.removers.push(removeEventListeners);
-  });
-
-  removeEventListeners () {
-    this.removers.forEach(listeners => listeners());
-    this.removers = [];
-  }
-
-  hasSize () {
-    const { width, height } = this.props;
-    return typeof width === 'number' && typeof height === 'number';
-  }
-
-  setCanvasRef = c => {
-    this.canvas = c;
-    if (c) this.setState({ width: c.offsetWidth });
-  };
-
-  render () {
-    const { alt, lang, src, width, height, onClick, zoomedIn } = this.props;
-    const { loading } = this.state;
-
-    const className = classNames('image-loader', {
-      'image-loader--loading': loading,
-      'image-loader--amorphous': !this.hasSize(),
-    });
-
-    return (
-      <div className={className}>
-        {loading ? (
-          <>
-            <div className='loading-bar__container' style={{ width: this.state.width || width }}>
-              <LoadingBar className='loading-bar' loading={1} />
-            </div>
-
-            <canvas
-              className='image-loader__preview-canvas'
-              ref={this.setCanvasRef}
-              width={width}
-              height={height}
-            />
-          </>
-        ) : (
-          <ZoomableImage
-            alt={alt}
-            lang={lang}
-            src={src}
-            onClick={onClick}
-            width={width}
-            height={height}
-            zoomedIn={zoomedIn}
-          />
-        )}
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/ui/components/image_modal.jsx b/app/javascript/mastodon/features/ui/components/image_modal.jsx
deleted file mode 100644
index f08ce15342..0000000000
--- a/app/javascript/mastodon/features/ui/components/image_modal.jsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { defineMessages, injectIntl } from 'react-intl';
-
-import classNames from 'classnames';
-
-import CloseIcon from '@/material-icons/400-24px/close.svg?react';
-import { IconButton } from 'mastodon/components/icon_button';
-
-import ImageLoader from './image_loader';
-
-const messages = defineMessages({
-  close: { id: 'lightbox.close', defaultMessage: 'Close' },
-});
-
-class ImageModal extends PureComponent {
-
-  static propTypes = {
-    src: PropTypes.string.isRequired,
-    alt: PropTypes.string.isRequired,
-    onClose: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-  };
-
-  state = {
-    navigationHidden: false,
-  };
-
-  toggleNavigation = () => {
-    this.setState(prevState => ({
-      navigationHidden: !prevState.navigationHidden,
-    }));
-  };
-
-  render () {
-    const { intl, src, alt, onClose } = this.props;
-    const { navigationHidden } = this.state;
-
-    const navigationClassName = classNames('media-modal__navigation', {
-      'media-modal__navigation--hidden': navigationHidden,
-    });
-
-    return (
-      <div className='modal-root__modal media-modal'>
-        <div className='media-modal__closer' role='presentation' onClick={onClose} >
-          <ImageLoader
-            src={src}
-            width={400}
-            height={400}
-            alt={alt}
-            onClick={this.toggleNavigation}
-          />
-        </div>
-
-        <div className={navigationClassName}>
-          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={40} />
-        </div>
-      </div>
-    );
-  }
-
-}
-
-export default injectIntl(ImageModal);
diff --git a/app/javascript/mastodon/features/ui/components/image_modal.tsx b/app/javascript/mastodon/features/ui/components/image_modal.tsx
new file mode 100644
index 0000000000..fa94cfcc3c
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/image_modal.tsx
@@ -0,0 +1,61 @@
+import { useCallback, useState } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import CloseIcon from '@/material-icons/400-24px/close.svg?react';
+import { IconButton } from 'mastodon/components/icon_button';
+
+import { ZoomableImage } from './zoomable_image';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+export const ImageModal: React.FC<{
+  src: string;
+  alt: string;
+  onClose: () => void;
+}> = ({ src, alt, onClose }) => {
+  const intl = useIntl();
+  const [navigationHidden, setNavigationHidden] = useState(false);
+
+  const toggleNavigation = useCallback(() => {
+    setNavigationHidden((prevState) => !prevState);
+  }, [setNavigationHidden]);
+
+  const navigationClassName = classNames('media-modal__navigation', {
+    'media-modal__navigation--hidden': navigationHidden,
+  });
+
+  return (
+    <div className='modal-root__modal media-modal'>
+      <div
+        className='media-modal__closer'
+        role='presentation'
+        onClick={onClose}
+      >
+        <ZoomableImage
+          src={src}
+          width={400}
+          height={400}
+          alt={alt}
+          onClick={toggleNavigation}
+        />
+      </div>
+
+      <div className={navigationClassName}>
+        <div className='media-modal__buttons'>
+          <IconButton
+            className='media-modal__close'
+            title={intl.formatMessage(messages.close)}
+            icon='times'
+            iconComponent={CloseIcon}
+            onClick={onClose}
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.jsx b/app/javascript/mastodon/features/ui/components/media_modal.jsx
index d69ceba539..9eb08ce9c3 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/media_modal.jsx
@@ -19,10 +19,10 @@ import { GIFV } from 'mastodon/components/gifv';
 import { Icon }  from 'mastodon/components/icon';
 import { IconButton } from 'mastodon/components/icon_button';
 import Footer from 'mastodon/features/picture_in_picture/components/footer';
-import Video from 'mastodon/features/video';
+import { Video } from 'mastodon/features/video';
 import { disableSwiping } from 'mastodon/initial_state';
 
-import ImageLoader from './image_loader';
+import { ZoomableImage } from './zoomable_image';
 
 const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
@@ -59,6 +59,12 @@ class MediaModal extends ImmutablePureComponent {
     }));
   };
 
+  handleZoomChange = (zoomedIn) => {
+    this.setState({
+      zoomedIn,
+    });
+  };
+
   handleSwipe = (index) => {
     this.setState({
       index: index % this.props.media.size,
@@ -165,23 +171,26 @@ class MediaModal extends ImmutablePureComponent {
     const leftNav  = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>;
     const rightNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav  media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>;
 
-    const content = media.map((image) => {
+    const content = media.map((image, idx) => {
       const width  = image.getIn(['meta', 'original', 'width']) || null;
       const height = image.getIn(['meta', 'original', 'height']) || null;
       const description = image.getIn(['translation', 'description']) || image.get('description');
 
       if (image.get('type') === 'image') {
         return (
-          <ImageLoader
-            previewSrc={image.get('preview_url')}
+          <ZoomableImage
             src={image.get('url')}
+            blurhash={image.get('blurhash')}
             width={width}
             height={height}
             alt={description}
             lang={lang}
             key={image.get('url')}
             onClick={this.handleToggleNavigation}
-            zoomedIn={zoomedIn}
+            onDoubleClick={this.handleZoomClick}
+            onClose={onClose}
+            onZoomChange={this.handleZoomChange}
+            zoomedIn={zoomedIn && idx === index}
           />
         );
       } else if (image.get('type') === 'video') {
@@ -196,9 +205,9 @@ class MediaModal extends ImmutablePureComponent {
             height={image.get('height')}
             frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
             aspectRatio={`${image.getIn(['meta', 'original', 'width'])} / ${image.getIn(['meta', 'original', 'height'])}`}
-            currentTime={currentTime || 0}
-            autoPlay={autoPlay || false}
-            volume={volume || 1}
+            startTime={currentTime || 0}
+            startPlaying={autoPlay || false}
+            startVolume={volume || 1}
             onCloseVideo={onClose}
             detailed
             alt={description}
@@ -262,7 +271,7 @@ class MediaModal extends ImmutablePureComponent {
             onChangeIndex={this.handleSwipe}
             onTransitionEnd={this.handleTransitionEnd}
             index={index}
-            disabled={disableSwiping}
+            disabled={disableSwiping || zoomedIn}
           >
             {content}
           </ReactSwipeableViews>
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx
index 5b2c2fc8d7..4dac46681d 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.jsx
+++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx
@@ -45,7 +45,7 @@ import {
   ConfirmFollowToListModal,
   ConfirmMissingAltTextModal,
 } from './confirmation_modals';
-import ImageModal from './image_modal';
+import { ImageModal } from './image_modal';
 import MediaModal from './media_modal';
 import { ModalPlaceholder } from './modal_placeholder';
 import VideoModal from './video_modal';
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index 95828ef25e..f3bf123415 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -1,10 +1,7 @@
 import PropTypes from 'prop-types';
 import { Component, useEffect } from 'react';
-
 import { defineMessages, injectIntl, useIntl } from 'react-intl';
-
 import { Link } from 'react-router-dom';
-
 import { useSelector, useDispatch } from 'react-redux';
 
 import CirclesIcon from '@/material-icons/400-24px/account_circle-fill.svg?react';
@@ -13,6 +10,8 @@ import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?re
 import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react';
 import ExploreActiveIcon from '@/material-icons/400-24px/explore-fill.svg?react';
 import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
+import HashtagIcon from '@/material-icons/400-24px/tag.svg?react';
+import DirectoryIcon from '@/material-icons/400-24px/group.svg?react';
 import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react';
 import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
 import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react';
@@ -31,6 +30,7 @@ import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
 import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react';
 import StarIcon from '@/material-icons/400-24px/star.svg?react';
 import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
+
 import { fetchFollowRequests } from 'mastodon/actions/accounts';
 import { IconWithBadge } from 'mastodon/components/icon_with_badge';
 import { WordmarkLogo } from 'mastodon/components/logo';
@@ -50,6 +50,8 @@ const messages = defineMessages({
   home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
   explore: { id: 'explore.title', defaultMessage: 'Explore' },
+  followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
+  directory: { id: 'navigation_bar.directory', defaultMessage: 'Profiles directory' },
   local: { id: 'column.local', defaultMessage: 'Local' },
   deepLocal: { id: 'column.deep_local', defaultMessage: 'Deep' },
   firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
@@ -71,7 +73,6 @@ const messages = defineMessages({
 });
 
 const NotificationsLink = () => {
-
   const count = useSelector(selectUnreadNotificationGroupsCount);
   const intl = useIntl();
 
@@ -129,25 +130,20 @@ class NavigationPanel extends Component {
     const { intl } = this.props;
     const { signedIn, disabledAccountId, permissions } = this.props.identity;
 
-    const explorer = (trendsEnabled ? (
+    const explorer = trendsEnabled ? (
       <ColumnLink transparent to='/explore' icon='explore' iconComponent={ExploreIcon} activeIconComponent={ExploreActiveIcon} text={intl.formatMessage(messages.explore)} />
     ) : (
       <ColumnLink transparent to='/search' icon='search' iconComponent={SearchIcon} text={intl.formatMessage(messages.search)} />
-    ));
-    
-    let banner = undefined;
+    );
 
-    if (transientSingleColumn) {
-      banner = (
-        <div className='switch-to-advanced'>
-          {intl.formatMessage(messages.openedInClassicInterface)}
-          {" "}
-          <a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
-            {intl.formatMessage(messages.advancedInterface)}
-          </a>
-        </div>
-      );
-    }
+    const banner = transientSingleColumn ? (
+      <div className='switch-to-advanced'>
+        {intl.formatMessage(messages.openedInClassicInterface)}{' '}
+        <a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
+          {intl.formatMessage(messages.advancedInterface)}
+        </a>
+      </div>
+    ) : null;
 
     return (
       <div className='navigation-panel'>
@@ -155,90 +151,59 @@ class NavigationPanel extends Component {
           <Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
         </div>
 
-        {banner &&
-          <div className='navigation-panel__banner'>
-            {banner}
-          </div>
-        }
+        {banner && <div className='navigation-panel__banner'>{banner}</div>}
 
         <div className='navigation-panel__menu'>
           {signedIn && (
             <>
               <ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
               <NotificationsLink />
-            </>
-          )}
-
-          {signedIn && enableLocalTimeline && (
-            <ColumnLink transparent to='/public/local/fixed' icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.local)} />
-          )}
-
-          {signedIn && enableDtlMenu && dtlTag && (
-            <ColumnLink transparent to={`/tags/${dtlTag}`} icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.deepLocal)} />
-          )}
-
-          {!signedIn && explorer}
-
-          {signedIn && (
-            <ColumnLink transparent to='/public' isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
-          )}
-
-          {(!signedIn && timelinePreview) && (
-            <ColumnLink transparent to={enableLocalTimeline ? '/public/local' : '/public'} isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
-          )}
-
-          {signedIn && (
-            <>
+              {enableLocalTimeline && <ColumnLink transparent to='/public/local/fixed' icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.local)} />}
+              {enableDtlMenu && dtlTag && <ColumnLink transparent to={`/tags/${dtlTag}`} icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.deepLocal)} />}
+              <ColumnLink transparent to='/public' isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
               <ListPanel />
               <hr />
-            </>
-          )}
-
-          {signedIn && (
-            <>
               <ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} />
               <ColumnLink transparent to='/antennas' icon='wifi' iconComponent={AntennaIcon} text={intl.formatMessage(messages.antennas)} isActive={this.isAntennasActive} />
               <ColumnLink transparent to='/circles' icon='user-circle' iconComponent={CirclesIcon} text={intl.formatMessage(messages.circles)} />
+              <ColumnLink transparent to='/followed_tags' icon='tag' iconComponent={HashtagIcon} text={intl.formatMessage(messages.followed_tags)} />
+              <ColumnLink transparent to='/directory' icon='group' iconComponent={DirectoryIcon} text={intl.formatMessage(messages.directory)} />
               <FollowRequestsLink />
               <ColumnLink transparent to='/conversations' icon='at' iconComponent={AlternateEmailIcon} text={intl.formatMessage(messages.direct)} />
-            </>
-          )}
-
-          {signedIn && explorer}
-
-          {signedIn && (
-            <>
+              {explorer}
               <ColumnLink transparent to='/bookmark_categories' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} />
-              { !isHideItem('favourite_menu') && <ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} /> }
+              {!isHideItem('favourite_menu') && <ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} />}
               <hr />
-
               <ColumnLink transparent href='/settings/preferences' icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} />
-
               {canManageReports(permissions) && <ColumnLink transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
               {canViewAdminDashboard(permissions) && <ColumnLink transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
             </>
           )}
 
           {!signedIn && (
-            <div className='navigation-panel__sign-in-banner'>
-              <hr />
-              { disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner /> }
-            </div>
+            <>
+              {explorer}
+              {(timelinePreview || enableLocalTimeline) && (
+                <ColumnLink transparent to={enableLocalTimeline ? '/public/local' : '/public'} isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
+              )}
+              <div className='navigation-panel__sign-in-banner'>
+                <hr />
+                {disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner />}
+              </div>
+            </>
           )}
+        </div>
 
-          <div className='navigation-panel__legal'>
-            <hr />
-            <ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} />
-          </div>
+        <div className='navigation-panel__legal'>
+          <hr />
+          <ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} />
         </div>
 
         <div className='flex-spacer' />
-
         <NavigationPortal />
       </div>
     );
   }
-
 }
 
 export default injectIntl(withIdentity(NavigationPanel));
diff --git a/app/javascript/mastodon/features/ui/components/upload_area.jsx b/app/javascript/mastodon/features/ui/components/upload_area.jsx
deleted file mode 100644
index b2702d35ef..0000000000
--- a/app/javascript/mastodon/features/ui/components/upload_area.jsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { FormattedMessage } from 'react-intl';
-
-import spring from 'react-motion/lib/spring';
-
-import Motion from '../util/optional_motion';
-
-export default class UploadArea extends PureComponent {
-
-  static propTypes = {
-    active: PropTypes.bool,
-    onClose: PropTypes.func,
-  };
-
-  handleKeyUp = (e) => {
-    const keyCode = e.keyCode;
-    if (this.props.active) {
-      switch(keyCode) {
-      case 27:
-        e.preventDefault();
-        e.stopPropagation();
-        this.props.onClose();
-        break;
-      }
-    }
-  };
-
-  componentDidMount () {
-    window.addEventListener('keyup', this.handleKeyUp, false);
-  }
-
-  componentWillUnmount () {
-    window.removeEventListener('keyup', this.handleKeyUp);
-  }
-
-  render () {
-    const { active } = this.props;
-
-    return (
-      <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
-        {({ backgroundOpacity, backgroundScale }) => (
-          <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
-            <div className='upload-area__drop'>
-              <div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} />
-              <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
-            </div>
-          </div>
-        )}
-      </Motion>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/ui/components/upload_area.tsx b/app/javascript/mastodon/features/ui/components/upload_area.tsx
new file mode 100644
index 0000000000..87ac090e7e
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/upload_area.tsx
@@ -0,0 +1,78 @@
+import { useCallback, useEffect } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { animated, config, useSpring } from '@react-spring/web';
+
+import { reduceMotion } from 'mastodon/initial_state';
+
+interface UploadAreaProps {
+  active?: boolean;
+  onClose: () => void;
+}
+
+export const UploadArea: React.FC<UploadAreaProps> = ({ active, onClose }) => {
+  const handleKeyUp = useCallback(
+    (e: KeyboardEvent) => {
+      if (active && e.key === 'Escape') {
+        e.preventDefault();
+        e.stopPropagation();
+        onClose();
+      }
+    },
+    [active, onClose],
+  );
+
+  useEffect(() => {
+    window.addEventListener('keyup', handleKeyUp, false);
+
+    return () => {
+      window.removeEventListener('keyup', handleKeyUp);
+    };
+  }, [handleKeyUp]);
+
+  const wrapperAnimStyles = useSpring({
+    from: {
+      opacity: 0,
+    },
+    to: {
+      opacity: 1,
+    },
+    reverse: !active,
+    immediate: reduceMotion,
+  });
+  const backgroundAnimStyles = useSpring({
+    from: {
+      transform: 'scale(0.95)',
+    },
+    to: {
+      transform: 'scale(1)',
+    },
+    reverse: !active,
+    config: config.wobbly,
+    immediate: reduceMotion,
+  });
+
+  return (
+    <animated.div
+      className='upload-area'
+      style={{
+        ...wrapperAnimStyles,
+        visibility: active ? 'visible' : 'hidden',
+      }}
+    >
+      <div className='upload-area__drop'>
+        <animated.div
+          className='upload-area__background'
+          style={backgroundAnimStyles}
+        />
+        <div className='upload-area__content'>
+          <FormattedMessage
+            id='upload_area.title'
+            defaultMessage='Drag & drop to upload'
+          />
+        </div>
+      </div>
+    </animated.div>
+  );
+};
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.jsx b/app/javascript/mastodon/features/ui/components/video_modal.jsx
index 4fc3ee1728..ed58d642a4 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/video_modal.jsx
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
 
 import { getAverageFromBlurhash } from 'mastodon/blurhash';
 import Footer from 'mastodon/features/picture_in_picture/components/footer';
-import Video from 'mastodon/features/video';
+import { Video } from 'mastodon/features/video';
 
 const mapStateToProps = (state, { statusId }) => ({
   status: state.getIn(['statuses', statusId]),
@@ -56,9 +56,9 @@ class VideoModal extends ImmutablePureComponent {
             aspectRatio={`${media.getIn(['meta', 'original', 'width'])} / ${media.getIn(['meta', 'original', 'height'])}`}
             blurhash={media.get('blurhash')}
             src={media.get('url')}
-            currentTime={options.startTime}
-            autoPlay={options.autoPlay}
-            volume={options.defaultVolume}
+            startTime={options.startTime}
+            startPlaying={options.autoPlay}
+            startVolume={options.defaultVolume}
             onCloseVideo={onClose}
             autoFocus
             detailed
diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.jsx b/app/javascript/mastodon/features/ui/components/zoomable_image.jsx
deleted file mode 100644
index c4129bf260..0000000000
--- a/app/javascript/mastodon/features/ui/components/zoomable_image.jsx
+++ /dev/null
@@ -1,402 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-const MIN_SCALE = 1;
-const MAX_SCALE = 4;
-const NAV_BAR_HEIGHT = 66;
-
-const getMidpoint = (p1, p2) => ({
-  x: (p1.clientX + p2.clientX) / 2,
-  y: (p1.clientY + p2.clientY) / 2,
-});
-
-const getDistance = (p1, p2) =>
-  Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
-
-const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
-
-// Normalizing mousewheel speed across browsers
-// copy from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
-const normalizeWheel = event => {
-  // Reasonable defaults
-  const PIXEL_STEP = 10;
-  const LINE_HEIGHT = 40;
-  const PAGE_HEIGHT = 800;
-
-  let sX = 0,
-    sY = 0, // spinX, spinY
-    pX = 0,
-    pY = 0; // pixelX, pixelY
-
-  // Legacy
-  if ('detail' in event) {
-    sY = event.detail;
-  }
-  if ('wheelDelta' in event) {
-    sY = -event.wheelDelta / 120;
-  }
-  if ('wheelDeltaY' in event) {
-    sY = -event.wheelDeltaY / 120;
-  }
-  if ('wheelDeltaX' in event) {
-    sX = -event.wheelDeltaX / 120;
-  }
-
-  // side scrolling on FF with DOMMouseScroll
-  if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
-    sX = sY;
-    sY = 0;
-  }
-
-  pX = sX * PIXEL_STEP;
-  pY = sY * PIXEL_STEP;
-
-  if ('deltaY' in event) {
-    pY = event.deltaY;
-  }
-  if ('deltaX' in event) {
-    pX = event.deltaX;
-  }
-
-  if ((pX || pY) && event.deltaMode) {
-    if (event.deltaMode === 1) { // delta in LINE units
-      pX *= LINE_HEIGHT;
-      pY *= LINE_HEIGHT;
-    } else { // delta in PAGE units
-      pX *= PAGE_HEIGHT;
-      pY *= PAGE_HEIGHT;
-    }
-  }
-
-  // Fall-back if spin cannot be determined
-  if (pX && !sX) {
-    sX = (pX < 1) ? -1 : 1;
-  }
-  if (pY && !sY) {
-    sY = (pY < 1) ? -1 : 1;
-  }
-
-  return {
-    spinX: sX,
-    spinY: sY,
-    pixelX: pX,
-    pixelY: pY,
-  };
-};
-
-class ZoomableImage extends PureComponent {
-
-  static propTypes = {
-    alt: PropTypes.string,
-    lang: PropTypes.string,
-    src: PropTypes.string.isRequired,
-    width: PropTypes.number,
-    height: PropTypes.number,
-    onClick: PropTypes.func,
-    zoomedIn: PropTypes.bool,
-  };
-
-  static defaultProps = {
-    alt: '',
-    lang: '',
-    width: null,
-    height: null,
-  };
-
-  state = {
-    scale: MIN_SCALE,
-    zoomMatrix: {
-      type: null, // 'width' 'height'
-      fullScreen: null, // bool
-      rate: null, // full screen scale rate
-      clientWidth: null,
-      clientHeight: null,
-      offsetWidth: null,
-      offsetHeight: null,
-      clientHeightFixed: null,
-      scrollTop: null,
-      scrollLeft: null,
-      translateX: null,
-      translateY: null,
-    },
-    dragPosition: { top: 0, left: 0, x: 0, y: 0 },
-    dragged: false,
-    lockScroll: { x: 0, y: 0 },
-    lockTranslate: { x: 0, y: 0 },
-  };
-
-  removers = [];
-  container = null;
-  image = null;
-  lastTouchEndTime = 0;
-  lastDistance = 0;
-
-  componentDidMount () {
-    let handler = this.handleTouchStart;
-    this.container.addEventListener('touchstart', handler);
-    this.removers.push(() => this.container.removeEventListener('touchstart', handler));
-    handler = this.handleTouchMove;
-    // on Chrome 56+, touch event listeners will default to passive
-    // https://www.chromestatus.com/features/5093566007214080
-    this.container.addEventListener('touchmove', handler, { passive: false });
-    this.removers.push(() => this.container.removeEventListener('touchend', handler));
-
-    handler = this.mouseDownHandler;
-    this.container.addEventListener('mousedown', handler);
-    this.removers.push(() => this.container.removeEventListener('mousedown', handler));
-
-    handler = this.mouseWheelHandler;
-    this.container.addEventListener('wheel', handler);
-    this.removers.push(() => this.container.removeEventListener('wheel', handler));
-    // Old Chrome
-    this.container.addEventListener('mousewheel', handler);
-    this.removers.push(() => this.container.removeEventListener('mousewheel', handler));
-    // Old Firefox
-    this.container.addEventListener('DOMMouseScroll', handler);
-    this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler));
-
-    this._initZoomMatrix();
-  }
-
-  componentWillUnmount () {
-    this._removeEventListeners();
-  }
-
-  componentDidUpdate (prevProps) {
-    if (prevProps.zoomedIn !== this.props.zoomedIn) {
-      this._toggleZoom();
-    }
-  }
-
-  _removeEventListeners () {
-    this.removers.forEach(listeners => listeners());
-    this.removers = [];
-  }
-
-  mouseWheelHandler = e => {
-    e.preventDefault();
-
-    const event = normalizeWheel(e);
-
-    if (this.state.zoomMatrix.type === 'width') {
-      // full width, scroll vertical
-      this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y);
-    } else {
-      // full height, scroll horizontal
-      this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelY, this.state.lockScroll.x);
-    }
-
-    // lock horizontal scroll
-    this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelX, this.state.lockScroll.x);
-  };
-
-  mouseDownHandler = e => {
-    this.setState({ dragPosition: {
-      left: this.container.scrollLeft,
-      top: this.container.scrollTop,
-      // Get the current mouse position
-      x: e.clientX,
-      y: e.clientY,
-    } });
-
-    this.image.addEventListener('mousemove', this.mouseMoveHandler);
-    this.image.addEventListener('mouseup', this.mouseUpHandler);
-  };
-
-  mouseMoveHandler = e => {
-    const dx = e.clientX - this.state.dragPosition.x;
-    const dy = e.clientY - this.state.dragPosition.y;
-
-    this.container.scrollLeft = Math.max(this.state.dragPosition.left - dx, this.state.lockScroll.x);
-    this.container.scrollTop = Math.max(this.state.dragPosition.top - dy, this.state.lockScroll.y);
-
-    this.setState({ dragged: true });
-  };
-
-  mouseUpHandler = () => {
-    this.image.removeEventListener('mousemove', this.mouseMoveHandler);
-    this.image.removeEventListener('mouseup', this.mouseUpHandler);
-  };
-
-  handleTouchStart = e => {
-    if (e.touches.length !== 2) return;
-
-    this.lastDistance = getDistance(...e.touches);
-  };
-
-  handleTouchMove = e => {
-    const { scrollTop, scrollHeight, clientHeight } = this.container;
-    if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
-      // prevent propagating event to MediaModal
-      e.stopPropagation();
-      return;
-    }
-    if (e.touches.length !== 2) return;
-
-    e.preventDefault();
-    e.stopPropagation();
-
-    const distance = getDistance(...e.touches);
-    const midpoint = getMidpoint(...e.touches);
-    const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate);
-    const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance);
-
-    this._zoom(scale, midpoint);
-
-    this.lastMidpoint = midpoint;
-    this.lastDistance = distance;
-  };
-
-  _zoom(nextScale, midpoint) {
-    const { scale, zoomMatrix } = this.state;
-    const { scrollLeft, scrollTop } = this.container;
-
-    // math memo:
-    // x = (scrollLeft + midpoint.x) / scrollWidth
-    // x' = (nextScrollLeft + midpoint.x) / nextScrollWidth
-    // scrollWidth = clientWidth * scale
-    // scrollWidth' = clientWidth * nextScale
-    // Solve x = x' for nextScrollLeft
-    const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
-    const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
-
-    this.setState({ scale: nextScale }, () => {
-      this.container.scrollLeft = nextScrollLeft;
-      this.container.scrollTop = nextScrollTop;
-      // reset the translateX/Y constantly
-      if (nextScale < zoomMatrix.rate) {
-        this.setState({
-          lockTranslate: {
-            x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
-            y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
-          },
-        });
-      }
-    });
-  }
-
-  handleClick = e => {
-    // don't propagate event to MediaModal
-    e.stopPropagation();
-    const dragged = this.state.dragged;
-    this.setState({ dragged: false });
-    if (dragged) return;
-    const handler = this.props.onClick;
-    if (handler) handler();
-  };
-
-  handleMouseDown = e => {
-    e.preventDefault();
-  };
-
-  _initZoomMatrix = () => {
-    const { width, height } = this.props;
-    const { clientWidth, clientHeight } = this.container;
-    const { offsetWidth, offsetHeight } = this.image;
-    const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT;
-
-    const type = width / height < clientWidth / clientHeightFixed ? 'width' : 'height';
-    const fullScreen = type === 'width' ?  width > clientWidth : height > clientHeightFixed;
-    const rate = type === 'width' ? Math.min(clientWidth, width) / offsetWidth : Math.min(clientHeightFixed, height) / offsetHeight;
-    const scrollTop = type === 'width' ?  (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2;
-    const scrollLeft = (clientWidth - offsetWidth) / 2;
-    const translateX = type === 'width' ? (width - offsetWidth) / (2 * rate) : 0;
-    const translateY = type === 'height' ? (height - offsetHeight) / (2 * rate) : 0;
-
-    this.setState({
-      zoomMatrix: {
-        type: type,
-        fullScreen: fullScreen,
-        rate: rate,
-        clientWidth: clientWidth,
-        clientHeight: clientHeight,
-        offsetWidth: offsetWidth,
-        offsetHeight: offsetHeight,
-        clientHeightFixed: clientHeightFixed,
-        scrollTop: scrollTop,
-        scrollLeft: scrollLeft,
-        translateX: translateX,
-        translateY: translateY,
-      },
-    });
-  };
-
-  _toggleZoom () {
-    const { scale, zoomMatrix } = this.state;
-
-    if ( scale >= zoomMatrix.rate ) {
-      this.setState({
-        scale: MIN_SCALE,
-        lockScroll: {
-          x: 0,
-          y: 0,
-        },
-        lockTranslate: {
-          x: 0,
-          y: 0,
-        },
-      }, () => {
-        this.container.scrollLeft = 0;
-        this.container.scrollTop = 0;
-      });
-    } else {
-      this.setState({
-        scale: zoomMatrix.rate,
-        lockScroll: {
-          x: zoomMatrix.scrollLeft,
-          y: zoomMatrix.scrollTop,
-        },
-        lockTranslate: {
-          x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX,
-          y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY,
-        },
-      }, () => {
-        this.container.scrollLeft = zoomMatrix.scrollLeft;
-        this.container.scrollTop = zoomMatrix.scrollTop;
-      });
-    }
-  }
-
-  setContainerRef = c => {
-    this.container = c;
-  };
-
-  setImageRef = c => {
-    this.image = c;
-  };
-
-  render () {
-    const { alt, lang, src, width, height } = this.props;
-    const { scale, lockTranslate, dragged } = this.state;
-    const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
-    const cursor = scale === MIN_SCALE ? null : (dragged ? 'grabbing' : 'grab');
-
-    return (
-      <div
-        className='zoomable-image'
-        ref={this.setContainerRef}
-        style={{ overflow, cursor, userSelect: 'none' }}
-      >
-        <img
-          role='presentation'
-          ref={this.setImageRef}
-          alt={alt}
-          title={alt}
-          lang={lang}
-          src={src}
-          width={width}
-          height={height}
-          style={{
-            transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
-            transformOrigin: '0 0',
-          }}
-          draggable={false}
-          onClick={this.handleClick}
-          onMouseDown={this.handleMouseDown}
-        />
-      </div>
-    );
-  }
-}
-
-export default ZoomableImage;
diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.tsx b/app/javascript/mastodon/features/ui/components/zoomable_image.tsx
new file mode 100644
index 0000000000..85e29e6aea
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/zoomable_image.tsx
@@ -0,0 +1,319 @@
+import { useState, useCallback, useRef, useEffect } from 'react';
+
+import classNames from 'classnames';
+
+import { useSpring, animated, config } from '@react-spring/web';
+import { createUseGesture, dragAction, pinchAction } from '@use-gesture/react';
+
+import { Blurhash } from 'mastodon/components/blurhash';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+
+const MIN_SCALE = 1;
+const MAX_SCALE = 4;
+const DOUBLE_CLICK_THRESHOLD = 250;
+
+interface ZoomMatrix {
+  containerWidth: number;
+  containerHeight: number;
+  imageWidth: number;
+  imageHeight: number;
+  initialScale: number;
+}
+
+const createZoomMatrix = (
+  container: HTMLElement,
+  image: HTMLImageElement,
+  fullWidth: number,
+  fullHeight: number,
+): ZoomMatrix => {
+  const { clientWidth, clientHeight } = container;
+  const { offsetWidth, offsetHeight } = image;
+
+  const type =
+    fullWidth / fullHeight < clientWidth / clientHeight ? 'width' : 'height';
+
+  const initialScale =
+    type === 'width'
+      ? Math.min(clientWidth, fullWidth) / offsetWidth
+      : Math.min(clientHeight, fullHeight) / offsetHeight;
+
+  return {
+    containerWidth: clientWidth,
+    containerHeight: clientHeight,
+    imageWidth: offsetWidth,
+    imageHeight: offsetHeight,
+    initialScale,
+  };
+};
+
+const useGesture = createUseGesture([dragAction, pinchAction]);
+
+const getBounds = (zoomMatrix: ZoomMatrix | null, scale: number) => {
+  if (!zoomMatrix || scale === MIN_SCALE) {
+    return {
+      left: -Infinity,
+      right: Infinity,
+      top: -Infinity,
+      bottom: Infinity,
+    };
+  }
+
+  const { containerWidth, containerHeight, imageWidth, imageHeight } =
+    zoomMatrix;
+
+  const bounds = {
+    left: -Math.max(imageWidth * scale - containerWidth, 0) / 2,
+    right: Math.max(imageWidth * scale - containerWidth, 0) / 2,
+    top: -Math.max(imageHeight * scale - containerHeight, 0) / 2,
+    bottom: Math.max(imageHeight * scale - containerHeight, 0) / 2,
+  };
+
+  return bounds;
+};
+
+interface ZoomableImageProps {
+  alt?: string;
+  lang?: string;
+  src: string;
+  width: number;
+  height: number;
+  onClick?: () => void;
+  onDoubleClick?: () => void;
+  onClose?: () => void;
+  onZoomChange?: (zoomedIn: boolean) => void;
+  zoomedIn?: boolean;
+  blurhash?: string;
+}
+
+export const ZoomableImage: React.FC<ZoomableImageProps> = ({
+  alt = '',
+  lang = '',
+  src,
+  width,
+  height,
+  onClick,
+  onDoubleClick,
+  onClose,
+  onZoomChange,
+  zoomedIn,
+  blurhash,
+}) => {
+  useEffect(() => {
+    const handler = (e: Event) => {
+      e.preventDefault();
+    };
+
+    document.addEventListener('gesturestart', handler);
+    document.addEventListener('gesturechange', handler);
+    document.addEventListener('gestureend', handler);
+
+    return () => {
+      document.removeEventListener('gesturestart', handler);
+      document.removeEventListener('gesturechange', handler);
+      document.removeEventListener('gestureend', handler);
+    };
+  }, []);
+
+  const [dragging, setDragging] = useState(false);
+  const [loaded, setLoaded] = useState(false);
+  const [error, setError] = useState(false);
+
+  const containerRef = useRef<HTMLDivElement>(null);
+  const imageRef = useRef<HTMLImageElement>(null);
+  const doubleClickTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
+  const zoomMatrixRef = useRef<ZoomMatrix | null>(null);
+
+  const [style, api] = useSpring(() => ({
+    x: 0,
+    y: 0,
+    scale: 1,
+    onRest: {
+      scale({ value }) {
+        if (!onZoomChange) {
+          return;
+        }
+        if (value === MIN_SCALE) {
+          onZoomChange(false);
+        } else {
+          onZoomChange(true);
+        }
+      },
+    },
+  }));
+
+  useGesture(
+    {
+      onDrag({
+        pinching,
+        cancel,
+        active,
+        last,
+        offset: [x, y],
+        velocity: [, vy],
+        direction: [, dy],
+        tap,
+      }) {
+        if (tap) {
+          if (!doubleClickTimeoutRef.current) {
+            doubleClickTimeoutRef.current = setTimeout(() => {
+              onClick?.();
+              doubleClickTimeoutRef.current = null;
+            }, DOUBLE_CLICK_THRESHOLD);
+          } else {
+            clearTimeout(doubleClickTimeoutRef.current);
+            doubleClickTimeoutRef.current = null;
+            onDoubleClick?.();
+          }
+
+          return;
+        }
+
+        if (!zoomedIn) {
+          // Swipe up/down to dismiss parent
+          if (last) {
+            if ((vy > 0.5 && dy !== 0) || Math.abs(y) > 150) {
+              onClose?.();
+            }
+
+            void api.start({ y: 0, config: config.wobbly });
+            return;
+          } else if (dy !== 0) {
+            void api.start({ y, immediate: true });
+            return;
+          }
+
+          cancel();
+          return;
+        }
+
+        if (pinching) {
+          cancel();
+          return;
+        }
+
+        if (active) {
+          setDragging(true);
+        } else {
+          setDragging(false);
+        }
+
+        void api.start({ x, y });
+      },
+
+      onPinch({ origin: [ox, oy], first, movement: [ms], offset: [s], memo }) {
+        if (!imageRef.current) {
+          return;
+        }
+
+        if (first) {
+          const { width, height, x, y } =
+            imageRef.current.getBoundingClientRect();
+          const tx = ox - (x + width / 2);
+          const ty = oy - (y + height / 2);
+
+          memo = [style.x.get(), style.y.get(), tx, ty];
+        }
+
+        const x = memo[0] - (ms - 1) * memo[2]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
+        const y = memo[1] - (ms - 1) * memo[3]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
+
+        void api.start({ scale: s, x, y });
+
+        return memo as [number, number, number, number];
+      },
+    },
+    {
+      target: imageRef,
+      drag: {
+        from: () => [style.x.get(), style.y.get()],
+        filterTaps: true,
+        bounds: () => getBounds(zoomMatrixRef.current, style.scale.get()),
+        rubberband: true,
+      },
+      pinch: {
+        scaleBounds: {
+          min: MIN_SCALE,
+          max: MAX_SCALE,
+        },
+        rubberband: true,
+      },
+    },
+  );
+
+  useEffect(() => {
+    if (!loaded || !containerRef.current || !imageRef.current) {
+      return;
+    }
+
+    zoomMatrixRef.current = createZoomMatrix(
+      containerRef.current,
+      imageRef.current,
+      width,
+      height,
+    );
+
+    if (!zoomedIn) {
+      void api.start({ scale: MIN_SCALE, x: 0, y: 0 });
+    } else if (style.scale.get() === MIN_SCALE) {
+      void api.start({ scale: zoomMatrixRef.current.initialScale, x: 0, y: 0 });
+    }
+  }, [api, style.scale, zoomedIn, width, height, loaded]);
+
+  const handleClick = useCallback((e: React.MouseEvent) => {
+    // This handler exists to cancel the onClick handler on the media modal which would
+    // otherwise close the modal. It cannot be used for actual click handling because
+    // we don't know if the user is about to pan the image or not.
+
+    e.preventDefault();
+    e.stopPropagation();
+  }, []);
+
+  const handleLoad = useCallback(() => {
+    setLoaded(true);
+  }, [setLoaded]);
+
+  const handleError = useCallback(() => {
+    setError(true);
+  }, [setError]);
+
+  return (
+    <div
+      className={classNames('zoomable-image', {
+        'zoomable-image--zoomed-in': zoomedIn,
+        'zoomable-image--error': error,
+        'zoomable-image--dragging': dragging,
+      })}
+      ref={containerRef}
+    >
+      {!loaded && blurhash && (
+        <div
+          className='zoomable-image__preview'
+          style={{
+            aspectRatio: `${width}/${height}`,
+            height: `min(${height}px, 100%)`,
+          }}
+        >
+          <Blurhash hash={blurhash} />
+        </div>
+      )}
+
+      <animated.img
+        style={style}
+        role='presentation'
+        ref={imageRef}
+        alt={alt}
+        title={alt}
+        lang={lang}
+        src={src}
+        width={width}
+        height={height}
+        draggable={false}
+        onLoad={handleLoad}
+        onError={handleError}
+        onClickCapture={handleClick}
+      />
+
+      {!loaded && !error && <LoadingIndicator />}
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/ui/containers/notifications_container.js b/app/javascript/mastodon/features/ui/containers/notifications_container.js
deleted file mode 100644
index b8aa9bc461..0000000000
--- a/app/javascript/mastodon/features/ui/containers/notifications_container.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { injectIntl } from 'react-intl';
-
-import { connect } from 'react-redux';
-
-import { NotificationStack } from 'react-notification';
-
-import { dismissAlert } from 'mastodon/actions/alerts';
-import { getAlerts } from 'mastodon/selectors';
-
-const mapStateToProps = (state, { intl }) => ({
-  notifications: getAlerts(state, { intl }),
-});
-
-const mapDispatchToProps = (dispatch) => ({
-  onDismiss (alert) {
-    dispatch(dismissAlert(alert));
-  },
-});
-
-export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index f2b4ee1194..7d190b3ce7 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -15,6 +15,7 @@ import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
 import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
 import { fetchNotifications } from 'mastodon/actions/notification_groups';
 import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
+import { AlertsController } from 'mastodon/components/alerts_controller';
 import { HoverCardController } from 'mastodon/components/hover_card_controller';
 import { PictureInPicture } from 'mastodon/features/picture_in_picture';
 import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
@@ -29,11 +30,11 @@ import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding
 
 import BundleColumnError from './components/bundle_column_error';
 import Header from './components/header';
-import UploadArea from './components/upload_area';
+import { UploadArea } from './components/upload_area';
+import { HashtagMenuController } from './components/hashtag_menu_controller';
 import ColumnsAreaContainer from './containers/columns_area_container';
 import LoadingBarContainer from './containers/loading_bar_container';
 import ModalContainer from './containers/modal_container';
-import NotificationsContainer from './containers/notifications_container';
 import {
   Compose,
   Status,
@@ -91,6 +92,7 @@ import {
   BookmarkCategoryEdit,
   ReactionDeck,
   TermsOfService,
+  AccountFeatured,
 } from './util/async-components';
 import { ColumnsContextProvider } from './util/columns_context';
 import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
@@ -224,7 +226,7 @@ class SwitchingColumnsArea extends PureComponent {
             <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
             <WrappedRoute path='/about' component={About} content={children} />
             <WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
-            <WrappedRoute path='/terms-of-service' component={TermsOfService} content={children} />
+            <WrappedRoute path='/terms-of-service/:date?' component={TermsOfService} content={children} />
 
             <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
             <Redirect from='/timelines/public' to='/public' exact />
@@ -272,6 +274,7 @@ class SwitchingColumnsArea extends PureComponent {
             <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
 
             <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
+            <WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
             <WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
             <WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
             <WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
@@ -656,8 +659,9 @@ class UI extends PureComponent {
           </SwitchingColumnsArea>
 
           {layout !== 'mobile' && <PictureInPicture />}
-          <NotificationsContainer />
+          <AlertsController />
           {!disableHoverCards && <HoverCardController />}
+          <HashtagMenuController />
           <LoadingBarContainer className='loading-bar' />
           <ModalContainer />
           <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index d8a1641c07..21dc902ae5 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -82,6 +82,10 @@ export function AccountGallery () {
   return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery');
 }
 
+export function AccountFeatured() {
+  return import(/* webpackChunkName: "features/account_featured" */'../../account_featured');
+}
+
 export function Followers () {
   return import(/* webpackChunkName: "features/followers" */'../../followers');
 }
diff --git a/app/javascript/mastodon/features/ui/util/fullscreen.js b/app/javascript/mastodon/features/ui/util/fullscreen.js
deleted file mode 100644
index cf5d0cf98d..0000000000
--- a/app/javascript/mastodon/features/ui/util/fullscreen.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// APIs for normalizing fullscreen operations. Note that Edge uses
-// the WebKit-prefixed APIs currently (as of Edge 16).
-
-export const isFullscreen = () => document.fullscreenElement ||
-  document.webkitFullscreenElement ||
-  document.mozFullScreenElement;
-
-export const exitFullscreen = () => {
-  if (document.exitFullscreen) {
-    document.exitFullscreen();
-  } else if (document.webkitExitFullscreen) {
-    document.webkitExitFullscreen();
-  } else if (document.mozCancelFullScreen) {
-    document.mozCancelFullScreen();
-  }
-};
-
-export const requestFullscreen = el => {
-  if (el.requestFullscreen) {
-    el.requestFullscreen();
-  } else if (el.webkitRequestFullscreen) {
-    el.webkitRequestFullscreen();
-  } else if (el.mozRequestFullScreen) {
-    el.mozRequestFullScreen();
-  }
-};
-
-export const attachFullscreenListener = (listener) => {
-  if ('onfullscreenchange' in document) {
-    document.addEventListener('fullscreenchange', listener);
-  } else if ('onwebkitfullscreenchange' in document) {
-    document.addEventListener('webkitfullscreenchange', listener);
-  } else if ('onmozfullscreenchange' in document) {
-    document.addEventListener('mozfullscreenchange', listener);
-  }
-};
-
-export const detachFullscreenListener = (listener) => {
-  if ('onfullscreenchange' in document) {
-    document.removeEventListener('fullscreenchange', listener);
-  } else if ('onwebkitfullscreenchange' in document) {
-    document.removeEventListener('webkitfullscreenchange', listener);
-  } else if ('onmozfullscreenchange' in document) {
-    document.removeEventListener('mozfullscreenchange', listener);
-  }
-};
diff --git a/app/javascript/mastodon/features/ui/util/fullscreen.ts b/app/javascript/mastodon/features/ui/util/fullscreen.ts
new file mode 100644
index 0000000000..796b9e6515
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/fullscreen.ts
@@ -0,0 +1,80 @@
+// APIs for normalizing fullscreen operations. Note that Edge uses
+// the WebKit-prefixed APIs currently (as of Edge 16).
+
+interface DocumentWithFullscreen extends Document {
+  mozFullScreenElement?: Element;
+  webkitFullscreenElement?: Element;
+  mozCancelFullScreen?: () => void;
+  webkitExitFullscreen?: () => void;
+}
+
+interface HTMLElementWithFullscreen extends HTMLElement {
+  mozRequestFullScreen?: () => void;
+  webkitRequestFullscreen?: () => void;
+}
+
+export const isFullscreen = () => {
+  const d = document as DocumentWithFullscreen;
+
+  return !!(
+    d.fullscreenElement ??
+    d.webkitFullscreenElement ??
+    d.mozFullScreenElement
+  );
+};
+
+export const exitFullscreen = () => {
+  const d = document as DocumentWithFullscreen;
+
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+  if (d.exitFullscreen) {
+    void d.exitFullscreen();
+  } else if (d.webkitExitFullscreen) {
+    d.webkitExitFullscreen();
+  } else if (d.mozCancelFullScreen) {
+    d.mozCancelFullScreen();
+  }
+};
+
+export const requestFullscreen = (el: HTMLElementWithFullscreen | null) => {
+  if (!el) {
+    return;
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+  if (el.requestFullscreen) {
+    void el.requestFullscreen();
+  } else if (el.webkitRequestFullscreen) {
+    el.webkitRequestFullscreen();
+  } else if (el.mozRequestFullScreen) {
+    el.mozRequestFullScreen();
+  }
+};
+
+export const attachFullscreenListener = (listener: () => void) => {
+  const d = document as DocumentWithFullscreen;
+
+  if ('onfullscreenchange' in d) {
+    d.addEventListener('fullscreenchange', listener);
+  } else if ('onwebkitfullscreenchange' in d) {
+    // @ts-expect-error This is valid on some browsers
+    d.addEventListener('webkitfullscreenchange', listener); // eslint-disable-line @typescript-eslint/no-unsafe-call
+  } else if ('onmozfullscreenchange' in d) {
+    // @ts-expect-error This is valid on some browsers
+    d.addEventListener('mozfullscreenchange', listener); // eslint-disable-line @typescript-eslint/no-unsafe-call
+  }
+};
+
+export const detachFullscreenListener = (listener: () => void) => {
+  const d = document as DocumentWithFullscreen;
+
+  if ('onfullscreenchange' in d) {
+    d.removeEventListener('fullscreenchange', listener);
+  } else if ('onwebkitfullscreenchange' in d) {
+    // @ts-expect-error This is valid on some browsers
+    d.removeEventListener('webkitfullscreenchange', listener); // eslint-disable-line @typescript-eslint/no-unsafe-call
+  } else if ('onmozfullscreenchange' in d) {
+    // @ts-expect-error This is valid on some browsers
+    d.removeEventListener('mozfullscreenchange', listener); // eslint-disable-line @typescript-eslint/no-unsafe-call
+  }
+};
diff --git a/app/javascript/mastodon/features/ui/util/optional_motion.js b/app/javascript/mastodon/features/ui/util/optional_motion.js
deleted file mode 100644
index 0b6d4d97f7..0000000000
--- a/app/javascript/mastodon/features/ui/util/optional_motion.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Motion from 'react-motion/lib/Motion';
-
-import { reduceMotion } from '../../../initial_state';
-
-import ReducedMotion from './reduced_motion';
-
-export default reduceMotion ? ReducedMotion : Motion;
diff --git a/app/javascript/mastodon/features/ui/util/reduced_motion.jsx b/app/javascript/mastodon/features/ui/util/reduced_motion.jsx
deleted file mode 100644
index fd044497f8..0000000000
--- a/app/javascript/mastodon/features/ui/util/reduced_motion.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-// Like react-motion's Motion, but reduces all animations to cross-fades
-// for the benefit of users with motion sickness.
-import PropTypes from 'prop-types';
-import { Component } from 'react';
-
-import Motion from 'react-motion/lib/Motion';
-
-const stylesToKeep = ['opacity', 'backgroundOpacity'];
-
-const extractValue = (value) => {
-  // This is either an object with a "val" property or it's a number
-  return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
-};
-
-class ReducedMotion extends Component {
-
-  static propTypes = {
-    defaultStyle: PropTypes.object,
-    style: PropTypes.object,
-    children: PropTypes.func,
-  };
-
-  render() {
-
-    const { style, defaultStyle, children } = this.props;
-
-    Object.keys(style).forEach(key => {
-      if (stylesToKeep.includes(key)) {
-        return;
-      }
-      // If it's setting an x or height or scale or some other value, we need
-      // to preserve the end-state value without actually animating it
-      style[key] = defaultStyle[key] = extractValue(style[key]);
-    });
-
-    return (
-      <Motion style={style} defaultStyle={defaultStyle}>
-        {children}
-      </Motion>
-    );
-  }
-
-}
-
-export default ReducedMotion;
diff --git a/app/javascript/mastodon/features/video/components/hotkey_indicator.tsx b/app/javascript/mastodon/features/video/components/hotkey_indicator.tsx
new file mode 100644
index 0000000000..1abbb96fc0
--- /dev/null
+++ b/app/javascript/mastodon/features/video/components/hotkey_indicator.tsx
@@ -0,0 +1,43 @@
+import { useIntl } from 'react-intl';
+import type { MessageDescriptor } from 'react-intl';
+
+import { useTransition, animated } from '@react-spring/web';
+
+import { Icon } from 'mastodon/components/icon';
+import type { IconProp } from 'mastodon/components/icon';
+
+export interface HotkeyEvent {
+  key: number;
+  icon: IconProp;
+  label: MessageDescriptor;
+}
+
+export const HotkeyIndicator: React.FC<{
+  events: HotkeyEvent[];
+  onDismiss: (e: HotkeyEvent) => void;
+}> = ({ events, onDismiss }) => {
+  const intl = useIntl();
+
+  const transitions = useTransition(events, {
+    from: { opacity: 0 },
+    keys: (item) => item.key,
+    enter: [{ opacity: 1 }],
+    leave: [{ opacity: 0 }],
+    onRest: (_result, _ctrl, item) => {
+      onDismiss(item);
+    },
+  });
+
+  return (
+    <>
+      {transitions((style, item) => (
+        <animated.div className='video-player__hotkey-indicator' style={style}>
+          <Icon id='' icon={item.icon} />
+          <span className='video-player__hotkey-indicator__label'>
+            {intl.formatMessage(item.label)}
+          </span>
+        </animated.div>
+      ))}
+    </>
+  );
+};
diff --git a/app/javascript/mastodon/features/video/index.jsx b/app/javascript/mastodon/features/video/index.jsx
deleted file mode 100644
index 89a8ba560a..0000000000
--- a/app/javascript/mastodon/features/video/index.jsx
+++ /dev/null
@@ -1,663 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import classNames from 'classnames';
-
-import { is } from 'immutable';
-
-import { throttle } from 'lodash';
-
-import FullscreenIcon from '@/material-icons/400-24px/fullscreen.svg?react';
-import FullscreenExitIcon from '@/material-icons/400-24px/fullscreen_exit.svg?react';
-import PauseIcon from '@/material-icons/400-24px/pause.svg?react';
-import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
-import RectangleIcon from '@/material-icons/400-24px/rectangle.svg?react';
-import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
-import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
-import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
-import { Blurhash } from 'mastodon/components/blurhash';
-import { Icon }  from 'mastodon/components/icon';
-import { playerSettings } from 'mastodon/settings';
-
-import { displayMedia, useBlurhash } from '../../initial_state';
-import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
-
-const messages = defineMessages({
-  play: { id: 'video.play', defaultMessage: 'Play' },
-  pause: { id: 'video.pause', defaultMessage: 'Pause' },
-  mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
-  unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
-  hide: { id: 'video.hide', defaultMessage: 'Hide video' },
-  expand: { id: 'video.expand', defaultMessage: 'Expand video' },
-  close: { id: 'video.close', defaultMessage: 'Close video' },
-  fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
-  exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
-});
-
-export const formatTime = secondsNum => {
-  let hours   = Math.floor(secondsNum / 3600);
-  let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
-  let seconds = secondsNum - (hours * 3600) - (minutes * 60);
-
-  if (hours   < 10) hours   = '0' + hours;
-  if (minutes < 10) minutes = '0' + minutes;
-  if (seconds < 10) seconds = '0' + seconds;
-
-  return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
-};
-
-export const findElementPosition = el => {
-  let box;
-
-  if (el.getBoundingClientRect && el.parentNode) {
-    box = el.getBoundingClientRect();
-  }
-
-  if (!box) {
-    return {
-      left: 0,
-      top: 0,
-    };
-  }
-
-  const docEl = document.documentElement;
-  const body  = document.body;
-
-  const clientLeft = docEl.clientLeft || body.clientLeft || 0;
-  const scrollLeft = window.pageXOffset || body.scrollLeft;
-  const left       = (box.left + scrollLeft) - clientLeft;
-
-  const clientTop = docEl.clientTop || body.clientTop || 0;
-  const scrollTop = window.pageYOffset || body.scrollTop;
-  const top       = (box.top + scrollTop) - clientTop;
-
-  return {
-    left: Math.round(left),
-    top: Math.round(top),
-  };
-};
-
-export const getPointerPosition = (el, event) => {
-  const position = {};
-  const box = findElementPosition(el);
-  const boxW = el.offsetWidth;
-  const boxH = el.offsetHeight;
-  const boxY = box.top;
-  const boxX = box.left;
-
-  let pageY = event.pageY;
-  let pageX = event.pageX;
-
-  if (event.changedTouches) {
-    pageX = event.changedTouches[0].pageX;
-    pageY = event.changedTouches[0].pageY;
-  }
-
-  position.y = Math.max(0, Math.min(1, (pageY - boxY) / boxH));
-  position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
-
-  return position;
-};
-
-export const fileNameFromURL = str => {
-  const url      = new URL(str);
-  const pathname = url.pathname;
-  const index    = pathname.lastIndexOf('/');
-
-  return pathname.slice(index + 1);
-};
-
-class Video extends PureComponent {
-
-  static propTypes = {
-    preview: PropTypes.string,
-    frameRate: PropTypes.string,
-    aspectRatio: PropTypes.string,
-    src: PropTypes.string.isRequired,
-    alt: PropTypes.string,
-    lang: PropTypes.string,
-    sensitive: PropTypes.bool,
-    currentTime: PropTypes.number,
-    onOpenVideo: PropTypes.func,
-    onCloseVideo: PropTypes.func,
-    detailed: PropTypes.bool,
-    editable: PropTypes.bool,
-    alwaysVisible: PropTypes.bool,
-    visible: PropTypes.bool,
-    onToggleVisibility: PropTypes.func,
-    deployPictureInPicture: PropTypes.func,
-    intl: PropTypes.object.isRequired,
-    blurhash: PropTypes.string,
-    autoPlay: PropTypes.bool,
-    volume: PropTypes.number,
-    muted: PropTypes.bool,
-    componentIndex: PropTypes.number,
-    autoFocus: PropTypes.bool,
-  };
-
-  static defaultProps = {
-    frameRate: '25',
-  };
-
-  state = {
-    currentTime: 0,
-    duration: 0,
-    volume: 0.5,
-    paused: true,
-    dragging: false,
-    fullscreen: false,
-    hovered: false,
-    muted: false,
-    revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
-  };
-
-  setPlayerRef = c => {
-    this.player = c;
-  };
-
-  setVideoRef = c => {
-    this.video = c;
-
-    if (this.video) {
-      this.setState({ volume: this.video.volume, muted: this.video.muted });
-    }
-  };
-
-  setSeekRef = c => {
-    this.seek = c;
-  };
-
-  setVolumeRef = c => {
-    this.volume = c;
-  };
-
-  handleClickRoot = e => e.stopPropagation();
-
-  handlePlay = () => {
-    this.setState({ paused: false });
-    this._updateTime();
-  };
-
-  handlePause = () => {
-    this.setState({ paused: true });
-  };
-
-  _updateTime () {
-    requestAnimationFrame(() => {
-      if (!this.video) return;
-
-      this.handleTimeUpdate();
-
-      if (!this.state.paused) {
-        this._updateTime();
-      }
-    });
-  }
-
-  handleTimeUpdate = () => {
-    this.setState({
-      currentTime: this.video.currentTime,
-      duration:this.video.duration,
-    });
-  };
-
-  handleVolumeMouseDown = e => {
-    document.addEventListener('mousemove', this.handleMouseVolSlide, true);
-    document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
-    document.addEventListener('touchmove', this.handleMouseVolSlide, true);
-    document.addEventListener('touchend', this.handleVolumeMouseUp, true);
-
-    this.handleMouseVolSlide(e);
-
-    e.preventDefault();
-    e.stopPropagation();
-  };
-
-  handleVolumeMouseUp = () => {
-    document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
-    document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
-    document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
-    document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
-  };
-
-  handleMouseVolSlide = throttle(e => {
-    const { x } = getPointerPosition(this.volume, e);
-
-    if(!isNaN(x)) {
-      this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
-        this._syncVideoToVolumeState(x);
-        this._saveVolumeState(x);
-      });
-    }
-  }, 15);
-
-  handleMouseDown = e => {
-    document.addEventListener('mousemove', this.handleMouseMove, true);
-    document.addEventListener('mouseup', this.handleMouseUp, true);
-    document.addEventListener('touchmove', this.handleMouseMove, true);
-    document.addEventListener('touchend', this.handleMouseUp, true);
-
-    this.setState({ dragging: true });
-    this.video.pause();
-    this.handleMouseMove(e);
-
-    e.preventDefault();
-    e.stopPropagation();
-  };
-
-  handleMouseUp = () => {
-    document.removeEventListener('mousemove', this.handleMouseMove, true);
-    document.removeEventListener('mouseup', this.handleMouseUp, true);
-    document.removeEventListener('touchmove', this.handleMouseMove, true);
-    document.removeEventListener('touchend', this.handleMouseUp, true);
-
-    this.setState({ dragging: false });
-    this.video.play();
-  };
-
-  handleMouseMove = throttle(e => {
-    const { x } = getPointerPosition(this.seek, e);
-    const currentTime = this.video.duration * x;
-
-    if (!isNaN(currentTime)) {
-      this.setState({ currentTime }, () => {
-        this.video.currentTime = currentTime;
-      });
-    }
-  }, 15);
-
-  seekBy (time) {
-    const currentTime = this.video.currentTime + time;
-
-    if (!isNaN(currentTime)) {
-      this.setState({ currentTime }, () => {
-        this.video.currentTime = currentTime;
-      });
-    }
-  }
-
-  handleVideoKeyDown = e => {
-    // On the video element or the seek bar, we can safely use the space bar
-    // for playback control because there are no buttons to press
-
-    if (e.key === ' ') {
-      e.preventDefault();
-      e.stopPropagation();
-      this.togglePlay();
-    }
-  };
-
-  handleKeyDown = e => {
-    const frameTime = 1 / this.getFrameRate();
-
-    switch(e.key) {
-    case 'k':
-      e.preventDefault();
-      e.stopPropagation();
-      this.togglePlay();
-      break;
-    case 'm':
-      e.preventDefault();
-      e.stopPropagation();
-      this.toggleMute();
-      break;
-    case 'f':
-      e.preventDefault();
-      e.stopPropagation();
-      this.toggleFullscreen();
-      break;
-    case 'j':
-      e.preventDefault();
-      e.stopPropagation();
-      this.seekBy(-10);
-      break;
-    case 'l':
-      e.preventDefault();
-      e.stopPropagation();
-      this.seekBy(10);
-      break;
-    case ',':
-      e.preventDefault();
-      e.stopPropagation();
-      this.seekBy(-frameTime);
-      break;
-    case '.':
-      e.preventDefault();
-      e.stopPropagation();
-      this.seekBy(frameTime);
-      break;
-    }
-
-    // If we are in fullscreen mode, we don't want any hotkeys
-    // interacting with the UI that's not visible
-
-    if (this.state.fullscreen) {
-      e.preventDefault();
-      e.stopPropagation();
-
-      if (e.key === 'Escape') {
-        exitFullscreen();
-      }
-    }
-  };
-
-  togglePlay = () => {
-    if (this.state.paused) {
-      this.setState({ paused: false }, () => this.video.play());
-    } else {
-      this.setState({ paused: true }, () => this.video.pause());
-    }
-  };
-
-  toggleFullscreen = () => {
-    if (isFullscreen()) {
-      exitFullscreen();
-    } else {
-      requestFullscreen(this.player);
-    }
-  };
-
-  componentDidMount () {
-    document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
-    document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
-    document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
-    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
-
-    window.addEventListener('scroll', this.handleScroll);
-
-    this._syncVideoFromLocalStorage();
-  }
-
-  componentWillUnmount () {
-    window.removeEventListener('scroll', this.handleScroll);
-
-    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
-    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
-    document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
-    document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
-
-    if (!this.state.paused && this.video && this.props.deployPictureInPicture) {
-      this.props.deployPictureInPicture('video', {
-        src: this.props.src,
-        currentTime: this.video.currentTime,
-        muted: this.video.muted,
-        volume: this.video.volume,
-      });
-    }
-  }
-
-  UNSAFE_componentWillReceiveProps (nextProps) {
-    if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
-      this.setState({ revealed: nextProps.visible });
-    }
-  }
-
-  componentDidUpdate (prevProps, prevState) {
-    if (prevState.revealed && !this.state.revealed && this.video) {
-      this.video.pause();
-    }
-  }
-
-  handleScroll = throttle(() => {
-    if (!this.video) {
-      return;
-    }
-
-    const { top, height } = this.video.getBoundingClientRect();
-    const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
-
-    if (!this.state.paused && !inView) {
-      this.video.pause();
-
-      if (this.props.deployPictureInPicture) {
-        this.props.deployPictureInPicture('video', {
-          src: this.props.src,
-          currentTime: this.video.currentTime,
-          muted: this.video.muted,
-          volume: this.video.volume,
-        });
-      }
-
-      this.setState({ paused: true });
-    }
-  }, 150, { trailing: true });
-
-  handleFullscreenChange = () => {
-    this.setState({ fullscreen: isFullscreen() });
-  };
-
-  handleMouseEnter = () => {
-    this.setState({ hovered: true });
-  };
-
-  handleMouseLeave = () => {
-    this.setState({ hovered: false });
-  };
-
-  toggleMute = () => {
-    const muted = !(this.video.muted || this.state.volume === 0);
-
-    this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
-      this._syncVideoToVolumeState();
-      this._saveVolumeState();
-    });
-  };
-
-  _syncVideoToVolumeState = (volume = null, muted = null) => {
-    if (!this.video) {
-      return;
-    }
-
-    this.video.volume = volume ?? this.state.volume;
-    this.video.muted = muted ?? this.state.muted;
-  };
-
-  _saveVolumeState = (volume = null, muted = null) => {
-    playerSettings.set('volume', volume ?? this.state.volume);
-    playerSettings.set('muted', muted ?? this.state.muted);
-  };
-
-  _syncVideoFromLocalStorage = () => {
-    this.setState({ volume: playerSettings.get('volume') ?? 0.5, muted: playerSettings.get('muted') ?? false }, () => {
-      this._syncVideoToVolumeState();
-    });
-  };
-
-  toggleReveal = () => {
-    if (this.props.onToggleVisibility) {
-      this.props.onToggleVisibility();
-    } else {
-      this.setState({ revealed: !this.state.revealed });
-    }
-  };
-
-  handleLoadedData = () => {
-    const { currentTime, volume, muted, autoPlay } = this.props;
-
-    if (currentTime) {
-      this.video.currentTime = currentTime;
-    }
-
-    if (volume !== undefined) {
-      this.video.volume = volume;
-    }
-
-    if (muted !== undefined) {
-      this.video.muted = muted;
-    }
-
-    if (autoPlay) {
-      this.video.play();
-    }
-  };
-
-  handleProgress = () => {
-    const lastTimeRange = this.video.buffered.length - 1;
-
-    if (lastTimeRange > -1) {
-      this.setState({ buffer: Math.ceil(this.video.buffered.end(lastTimeRange) / this.video.duration * 100) });
-    }
-  };
-
-  handleVolumeChange = () => {
-    this.setState({ volume: this.video.volume, muted: this.video.muted });
-    this._saveVolumeState(this.video.volume, this.video.muted);
-  };
-
-  handleOpenVideo = () => {
-    this.video.pause();
-
-    this.props.onOpenVideo(this.props.lang, {
-      startTime: this.video.currentTime,
-      autoPlay: !this.state.paused,
-      defaultVolume: this.state.volume,
-      componentIndex: this.props.componentIndex,
-    });
-  };
-
-  handleCloseVideo = () => {
-    this.video.pause();
-    this.props.onCloseVideo();
-  };
-
-  getFrameRate () {
-    if (this.props.frameRate && isNaN(this.props.frameRate)) {
-      // The frame rate is returned as a fraction string so we
-      // need to convert it to a number
-
-      return this.props.frameRate.split('/').reduce((p, c) => p / c);
-    }
-
-    return this.props.frameRate;
-  }
-
-  render () {
-    const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
-    const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
-    const progress = Math.min((currentTime / duration) * 100, 100);
-    const muted = this.state.muted || volume === 0;
-
-    let preload;
-
-    if (this.props.currentTime || fullscreen || dragging) {
-      preload = 'auto';
-    } else if (detailed) {
-      preload = 'metadata';
-    } else {
-      preload = 'none';
-    }
-
-    let warning;
-
-    if (sensitive) {
-      warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
-    } else {
-      warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
-    }
-
-    // The outer wrapper is necessary to avoid reflowing the layout when going into full screen
-    return (
-      <div style={{ aspectRatio }}>
-        <div
-          role='menuitem'
-          className={classNames('video-player', { inactive: !revealed, detailed, fullscreen, editable })}
-          style={{ aspectRatio }}
-          ref={this.setPlayerRef}
-          onMouseEnter={this.handleMouseEnter}
-          onMouseLeave={this.handleMouseLeave}
-          onClick={this.handleClickRoot}
-          onKeyDown={this.handleKeyDown}
-          tabIndex={0}
-        >
-          <Blurhash
-            hash={blurhash}
-            className={classNames('media-gallery__preview', {
-              'media-gallery__preview--hidden': revealed,
-            })}
-            dummy={!useBlurhash}
-          />
-
-          {(revealed || editable) && <video
-            ref={this.setVideoRef}
-            src={src}
-            poster={preview}
-            preload={preload}
-            role='button'
-            tabIndex={0}
-            aria-label={alt}
-            title={alt}
-            lang={lang}
-            onClick={this.togglePlay}
-            onKeyDown={this.handleVideoKeyDown}
-            onPlay={this.handlePlay}
-            onPause={this.handlePause}
-            onLoadedData={this.handleLoadedData}
-            onProgress={this.handleProgress}
-            onVolumeChange={this.handleVolumeChange}
-            style={{ width: '100%' }}
-          />}
-
-          <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
-            <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
-              <span className='spoiler-button__overlay__label'>
-                {warning}
-                <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
-              </span>
-            </button>
-          </div>
-
-          <div className={classNames('video-player__controls', { active: paused || hovered })}>
-            <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
-              <div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} />
-              <div className='video-player__seek__progress' style={{ width: `${progress}%` }} />
-
-              <span
-                className={classNames('video-player__seek__handle', { active: dragging })}
-                tabIndex={0}
-                style={{ left: `${progress}%` }}
-                onKeyDown={this.handleVideoKeyDown}
-              />
-            </div>
-
-            <div className='video-player__buttons-bar'>
-              <div className='video-player__buttons left'>
-                <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={autoFocus}><Icon id={paused ? 'play' : 'pause'} icon={paused ? PlayArrowIcon : PauseIcon} /></button>
-                <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} icon={muted ? VolumeOffIcon : VolumeUpIcon} /></button>
-
-                <div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
-                  <div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%` }} />
-
-                  <span
-                    className={classNames('video-player__volume__handle')}
-                    tabIndex={0}
-                    style={{ left: `${muted ? 0 : volume * 100}%` }}
-                  />
-                </div>
-
-                {(detailed || fullscreen) && (
-                  <span className='video-player__time'>
-                    <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
-                    <span className='video-player__time-sep'>/</span>
-                    <span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span>
-                  </span>
-                )}
-              </div>
-
-              <div className='video-player__buttons right'>
-                {(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>}
-                {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon id='expand' icon={RectangleIcon} /></button>}
-                {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon id='compress' icon={FullscreenExitIcon} /></button>}
-                <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} className='player-button' onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} icon={fullscreen ? FullscreenExitIcon : FullscreenIcon} /></button>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-export default injectIntl(Video);
diff --git a/app/javascript/mastodon/features/video/index.tsx b/app/javascript/mastodon/features/video/index.tsx
new file mode 100644
index 0000000000..fca6fd0250
--- /dev/null
+++ b/app/javascript/mastodon/features/video/index.tsx
@@ -0,0 +1,1045 @@
+import { useEffect, useCallback, useRef, useState } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { useSpring, animated, config } from '@react-spring/web';
+import { throttle } from 'lodash';
+
+import Forward5Icon from '@/material-icons/400-24px/forward_5-fill.svg?react';
+import FullscreenIcon from '@/material-icons/400-24px/fullscreen.svg?react';
+import FullscreenExitIcon from '@/material-icons/400-24px/fullscreen_exit.svg?react';
+import PauseIcon from '@/material-icons/400-24px/pause-fill.svg?react';
+import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
+import RectangleIcon from '@/material-icons/400-24px/rectangle.svg?react';
+import Replay5Icon from '@/material-icons/400-24px/replay_5-fill.svg?react';
+import VolumeDownIcon from '@/material-icons/400-24px/volume_down-fill.svg?react';
+import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
+import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
+import { Blurhash } from 'mastodon/components/blurhash';
+import { Icon } from 'mastodon/components/icon';
+import { SpoilerButton } from 'mastodon/components/spoiler_button';
+import {
+  isFullscreen,
+  requestFullscreen,
+  exitFullscreen,
+  attachFullscreenListener,
+  detachFullscreenListener,
+} from 'mastodon/features/ui/util/fullscreen';
+import {
+  displayMedia,
+  useBlurhash,
+  reduceMotion,
+} from 'mastodon/initial_state';
+import { playerSettings } from 'mastodon/settings';
+
+import { HotkeyIndicator } from './components/hotkey_indicator';
+import type { HotkeyEvent } from './components/hotkey_indicator';
+
+const messages = defineMessages({
+  play: { id: 'video.play', defaultMessage: 'Play' },
+  pause: { id: 'video.pause', defaultMessage: 'Pause' },
+  mute: { id: 'video.mute', defaultMessage: 'Mute' },
+  unmute: { id: 'video.unmute', defaultMessage: 'Unmute' },
+  hide: { id: 'video.hide', defaultMessage: 'Hide video' },
+  expand: { id: 'video.expand', defaultMessage: 'Expand video' },
+  close: { id: 'video.close', defaultMessage: 'Close video' },
+  fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
+  exit_fullscreen: {
+    id: 'video.exit_fullscreen',
+    defaultMessage: 'Exit full screen',
+  },
+  volumeUp: { id: 'video.volume_up', defaultMessage: 'Volume up' },
+  volumeDown: { id: 'video.volume_down', defaultMessage: 'Volume down' },
+  skipForward: { id: 'video.skip_forward', defaultMessage: 'Skip forward' },
+  skipBackward: { id: 'video.skip_backward', defaultMessage: 'Skip backward' },
+});
+
+const DOUBLE_CLICK_THRESHOLD = 250;
+const HOVER_FADE_DELAY = 4000;
+
+export const formatTime = (secondsNum: number) => {
+  const hours = Math.floor(secondsNum / 3600);
+  const minutes = Math.floor((secondsNum - hours * 3600) / 60);
+  const seconds = secondsNum - hours * 3600 - minutes * 60;
+
+  const formattedHours = `${hours < 10 ? '0' : ''}${hours}`;
+  const formattedMinutes = `${minutes < 10 ? '0' : ''}${minutes}`;
+  const formattedSeconds = `${seconds < 10 ? '0' : ''}${seconds}`;
+
+  return (
+    (formattedHours === '00' ? '' : `${formattedHours}:`) +
+    `${formattedMinutes}:${formattedSeconds}`
+  );
+};
+
+export const findElementPosition = (el: HTMLElement) => {
+  const box = el.getBoundingClientRect();
+  const docEl = document.documentElement;
+  const body = document.body;
+
+  const clientLeft = docEl.clientLeft || body.clientLeft || 0;
+  const scrollLeft = window.scrollX || body.scrollLeft;
+  const left = box.left + scrollLeft - clientLeft;
+
+  const clientTop = docEl.clientTop || body.clientTop || 0;
+  const scrollTop = window.scrollY || body.scrollTop;
+  const top = box.top + scrollTop - clientTop;
+
+  return {
+    left: Math.round(left),
+    top: Math.round(top),
+  };
+};
+
+export const getPointerPosition = (
+  el: HTMLElement | null,
+  event: MouseEvent,
+) => {
+  if (!el) {
+    return {
+      y: 0,
+      x: 0,
+    };
+  }
+
+  const box = findElementPosition(el);
+  const boxW = el.offsetWidth;
+  const boxH = el.offsetHeight;
+  const boxY = box.top;
+  const boxX = box.left;
+
+  const { pageY, pageX } = event;
+
+  return {
+    y: Math.max(0, Math.min(1, (pageY - boxY) / boxH)),
+    x: Math.max(0, Math.min(1, (pageX - boxX) / boxW)),
+  };
+};
+
+export const fileNameFromURL = (str: string) => {
+  const url = new URL(str);
+  const pathname = url.pathname;
+  const index = pathname.lastIndexOf('/');
+
+  return pathname.slice(index + 1);
+};
+
+const frameRateAsNumber = (frameRate: string): number => {
+  if (frameRate.includes('/')) {
+    return frameRate
+      .split('/')
+      .map((c) => parseInt(c))
+      .reduce((p, c) => p / c);
+  }
+
+  return parseInt(frameRate);
+};
+
+const persistVolume = (volume: number, muted: boolean) => {
+  playerSettings.set('volume', volume);
+  playerSettings.set('muted', muted);
+};
+
+const restoreVolume = (video: HTMLVideoElement) => {
+  const volume = (playerSettings.get('volume') as number | undefined) ?? 0.5;
+  const muted = (playerSettings.get('muted') as boolean | undefined) ?? false;
+
+  video.volume = volume;
+  video.muted = muted;
+};
+
+let hotkeyEventId = 0;
+
+const registerHotkeyEvent = (
+  setHotkeyEvents: React.Dispatch<React.SetStateAction<HotkeyEvent[]>>,
+  event: Omit<HotkeyEvent, 'key'>,
+) => {
+  setHotkeyEvents(() => [{ key: hotkeyEventId++, ...event }]);
+};
+
+export const Video: React.FC<{
+  preview?: string;
+  frameRate?: string;
+  aspectRatio?: string;
+  src: string;
+  alt?: string;
+  lang?: string;
+  sensitive?: boolean;
+  onOpenVideo?: (options: {
+    startTime: number;
+    autoPlay: boolean;
+    defaultVolume: number;
+  }) => void;
+  onCloseVideo?: () => void;
+  detailed?: boolean;
+  editable?: boolean;
+  alwaysVisible?: boolean;
+  visible?: boolean;
+  onToggleVisibility?: () => void;
+  deployPictureInPicture?: (
+    type: string,
+    mediaProps: {
+      src: string;
+      muted: boolean;
+      volume: number;
+      currentTime: number;
+    },
+  ) => void;
+  blurhash?: string;
+  startPlaying?: boolean;
+  startTime?: number;
+  startVolume?: number;
+  startMuted?: boolean;
+  matchedFilters?: string[];
+}> = ({
+  preview,
+  frameRate = '25',
+  aspectRatio,
+  src,
+  alt = '',
+  lang,
+  sensitive,
+  onOpenVideo,
+  onCloseVideo,
+  detailed,
+  editable,
+  alwaysVisible,
+  visible,
+  onToggleVisibility,
+  deployPictureInPicture,
+  blurhash,
+  startPlaying,
+  startTime,
+  startVolume,
+  startMuted,
+  matchedFilters,
+}) => {
+  const intl = useIntl();
+  const [currentTime, setCurrentTime] = useState(0);
+  const [duration, setDuration] = useState(0);
+  const [volume, setVolume] = useState(0.5);
+  const [paused, setPaused] = useState(true);
+  const [dragging, setDragging] = useState(false);
+  const [fullscreen, setFullscreen] = useState(false);
+  const [hovered, setHovered] = useState(false);
+  const [muted, setMuted] = useState(false);
+  const [revealed, setRevealed] = useState(false);
+  const [hotkeyEvents, setHotkeyEvents] = useState<HotkeyEvent[]>([]);
+
+  const playerRef = useRef<HTMLDivElement>(null);
+  const videoRef = useRef<HTMLVideoElement | null>(null);
+  const seekRef = useRef<HTMLDivElement>(null);
+  const volumeRef = useRef<HTMLDivElement>(null);
+  const doubleClickTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
+  const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
+
+  const [style, api] = useSpring(() => ({
+    progress: '0%',
+    buffer: '0%',
+    volume: '0%',
+  }));
+
+  const handleVideoRef = useCallback(
+    (c: HTMLVideoElement | null) => {
+      if (videoRef.current && !videoRef.current.paused && c === null) {
+        deployPictureInPicture?.('video', {
+          src: src,
+          currentTime: videoRef.current.currentTime,
+          muted: videoRef.current.muted,
+          volume: videoRef.current.volume,
+        });
+      }
+
+      videoRef.current = c;
+
+      if (videoRef.current) {
+        restoreVolume(videoRef.current);
+        setVolume(videoRef.current.volume);
+        setMuted(videoRef.current.muted);
+        void api.start({
+          volume: `${videoRef.current.volume * 100}%`,
+          immediate: reduceMotion,
+        });
+      }
+    },
+    [api, setVolume, setMuted, src, deployPictureInPicture],
+  );
+
+  const togglePlay = useCallback(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    if (videoRef.current.paused) {
+      void videoRef.current.play();
+    } else {
+      videoRef.current.pause();
+    }
+  }, []);
+
+  const toggleFullscreen = useCallback(() => {
+    if (isFullscreen()) {
+      exitFullscreen();
+    } else {
+      requestFullscreen(playerRef.current);
+    }
+  }, []);
+
+  const toggleMute = useCallback(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    const effectivelyMuted =
+      videoRef.current.muted || videoRef.current.volume === 0;
+
+    if (effectivelyMuted) {
+      videoRef.current.muted = false;
+
+      if (videoRef.current.volume === 0) {
+        videoRef.current.volume = 0.05;
+      }
+    } else {
+      videoRef.current.muted = true;
+    }
+  }, []);
+
+  const handleClickRoot = useCallback((e: React.MouseEvent) => {
+    // Stop clicks within the video player e.g. closing parent modal
+    e.stopPropagation();
+  }, []);
+
+  const handleClick = useCallback(() => {
+    if (!doubleClickTimeoutRef.current) {
+      doubleClickTimeoutRef.current = setTimeout(() => {
+        registerHotkeyEvent(setHotkeyEvents, {
+          icon: videoRef.current?.paused ? PlayArrowIcon : PauseIcon,
+          label: videoRef.current?.paused ? messages.play : messages.pause,
+        });
+        togglePlay();
+        doubleClickTimeoutRef.current = null;
+      }, DOUBLE_CLICK_THRESHOLD);
+    } else {
+      clearTimeout(doubleClickTimeoutRef.current);
+      doubleClickTimeoutRef.current = null;
+      registerHotkeyEvent(setHotkeyEvents, {
+        icon: isFullscreen() ? FullscreenExitIcon : FullscreenIcon,
+        label: isFullscreen() ? messages.exit_fullscreen : messages.fullscreen,
+      });
+      toggleFullscreen();
+    }
+  }, [setHotkeyEvents, togglePlay, toggleFullscreen]);
+
+  const handlePlay = useCallback(() => {
+    setPaused(false);
+  }, [setPaused]);
+
+  const handlePause = useCallback(() => {
+    setPaused(true);
+  }, [setPaused]);
+
+  useEffect(() => {
+    let nextFrame: ReturnType<typeof requestAnimationFrame>;
+
+    const updateProgress = () => {
+      nextFrame = requestAnimationFrame(() => {
+        if (videoRef.current) {
+          void api.start({
+            progress: `${(videoRef.current.currentTime / videoRef.current.duration) * 100}%`,
+            immediate: reduceMotion,
+            config: config.stiff,
+          });
+        }
+
+        updateProgress();
+      });
+    };
+
+    updateProgress();
+
+    return () => {
+      cancelAnimationFrame(nextFrame);
+    };
+  }, [api]);
+
+  useEffect(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    videoRef.current.volume = volume;
+    videoRef.current.muted = muted;
+  }, [volume, muted]);
+
+  useEffect(() => {
+    if (typeof visible !== 'undefined') {
+      setRevealed(visible);
+    } else {
+      setRevealed(
+        displayMedia === 'show_all' ||
+          (displayMedia !== 'hide_all' && !sensitive),
+      );
+    }
+  }, [visible, sensitive]);
+
+  useEffect(() => {
+    if (!revealed && videoRef.current) {
+      videoRef.current.pause();
+    }
+  }, [revealed]);
+
+  useEffect(() => {
+    const handleFullscreenChange = () => {
+      setFullscreen(isFullscreen());
+    };
+
+    const handleScroll = throttle(
+      () => {
+        if (!videoRef.current) {
+          return;
+        }
+
+        const { top, height } = videoRef.current.getBoundingClientRect();
+        const inView =
+          top <=
+            (window.innerHeight || document.documentElement.clientHeight) &&
+          top + height >= 0;
+
+        if (!videoRef.current.paused && !inView) {
+          videoRef.current.pause();
+
+          deployPictureInPicture?.('video', {
+            src: src,
+            currentTime: videoRef.current.currentTime,
+            muted: videoRef.current.muted,
+            volume: videoRef.current.volume,
+          });
+        }
+      },
+      150,
+      { trailing: true },
+    );
+
+    attachFullscreenListener(handleFullscreenChange);
+    window.addEventListener('scroll', handleScroll);
+
+    return () => {
+      window.removeEventListener('scroll', handleScroll);
+      detachFullscreenListener(handleFullscreenChange);
+    };
+  }, [setPaused, setFullscreen, src, deployPictureInPicture]);
+
+  const handleTimeUpdate = useCallback(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    setCurrentTime(videoRef.current.currentTime);
+  }, [setCurrentTime]);
+
+  const handleVolumeMouseDown = useCallback(
+    (e: React.MouseEvent) => {
+      const handleVolumeMouseUp = () => {
+        document.removeEventListener('mousemove', handleVolumeMouseMove, true);
+        document.removeEventListener('mouseup', handleVolumeMouseUp, true);
+      };
+
+      const handleVolumeMouseMove = (e: MouseEvent) => {
+        if (!volumeRef.current || !videoRef.current) {
+          return;
+        }
+
+        const { x } = getPointerPosition(volumeRef.current, e);
+
+        if (!isNaN(x)) {
+          videoRef.current.volume = x;
+          videoRef.current.muted = x > 0 ? false : true;
+          void api.start({ volume: `${x * 100}%`, immediate: true });
+        }
+      };
+
+      document.addEventListener('mousemove', handleVolumeMouseMove, true);
+      document.addEventListener('mouseup', handleVolumeMouseUp, true);
+
+      handleVolumeMouseMove(e.nativeEvent);
+
+      e.preventDefault();
+      e.stopPropagation();
+    },
+    [api],
+  );
+
+  const handleSeekMouseDown = useCallback(
+    (e: React.MouseEvent) => {
+      const handleSeekMouseUp = () => {
+        document.removeEventListener('mousemove', handleSeekMouseMove, true);
+        document.removeEventListener('mouseup', handleSeekMouseUp, true);
+
+        setDragging(false);
+        void videoRef.current?.play();
+      };
+
+      const handleSeekMouseMove = (e: MouseEvent) => {
+        if (!seekRef.current || !videoRef.current) {
+          return;
+        }
+
+        const { x } = getPointerPosition(seekRef.current, e);
+        const newTime = videoRef.current.duration * x;
+
+        if (!isNaN(newTime)) {
+          videoRef.current.currentTime = newTime;
+          void api.start({ progress: `${x * 100}%`, immediate: true });
+        }
+      };
+
+      document.addEventListener('mousemove', handleSeekMouseMove, true);
+      document.addEventListener('mouseup', handleSeekMouseUp, true);
+
+      setDragging(true);
+      videoRef.current?.pause();
+      handleSeekMouseMove(e.nativeEvent);
+
+      e.preventDefault();
+      e.stopPropagation();
+    },
+    [setDragging, api],
+  );
+
+  const seekBy = (time: number) => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    const newTime = videoRef.current.currentTime + time;
+
+    if (!isNaN(newTime)) {
+      videoRef.current.currentTime = newTime;
+    }
+  };
+
+  const updateVolumeBy = (step: number) => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    const newVolume = videoRef.current.volume + step;
+
+    if (!isNaN(newVolume)) {
+      videoRef.current.volume = newVolume;
+      videoRef.current.muted = newVolume > 0 ? false : true;
+    }
+  };
+
+  const handleVideoKeyDown = useCallback(
+    (e: React.KeyboardEvent) => {
+      // On the video element or the seek bar, we can safely use the space bar
+      // for playback control because there are no buttons to press
+
+      if (e.key === ' ') {
+        e.preventDefault();
+        e.stopPropagation();
+        registerHotkeyEvent(setHotkeyEvents, {
+          icon: videoRef.current?.paused ? PlayArrowIcon : PauseIcon,
+          label: videoRef.current?.paused ? messages.play : messages.pause,
+        });
+        togglePlay();
+      }
+    },
+    [setHotkeyEvents, togglePlay],
+  );
+
+  const handleKeyDown = useCallback(
+    (e: React.KeyboardEvent) => {
+      const frameTime = 1 / frameRateAsNumber(frameRate);
+
+      switch (e.key) {
+        case 'k':
+        case ' ':
+          e.preventDefault();
+          e.stopPropagation();
+          registerHotkeyEvent(setHotkeyEvents, {
+            icon: videoRef.current?.paused ? PlayArrowIcon : PauseIcon,
+            label: videoRef.current?.paused ? messages.play : messages.pause,
+          });
+          togglePlay();
+          break;
+        case 'm':
+          e.preventDefault();
+          e.stopPropagation();
+          registerHotkeyEvent(setHotkeyEvents, {
+            icon: videoRef.current?.muted ? VolumeUpIcon : VolumeOffIcon,
+            label: videoRef.current?.muted ? messages.unmute : messages.mute,
+          });
+          toggleMute();
+          break;
+        case 'f':
+          e.preventDefault();
+          e.stopPropagation();
+          registerHotkeyEvent(setHotkeyEvents, {
+            icon: isFullscreen() ? FullscreenExitIcon : FullscreenIcon,
+            label: isFullscreen()
+              ? messages.exit_fullscreen
+              : messages.fullscreen,
+          });
+          toggleFullscreen();
+          break;
+        case 'j':
+        case 'ArrowLeft':
+          e.preventDefault();
+          e.stopPropagation();
+          registerHotkeyEvent(setHotkeyEvents, {
+            icon: Replay5Icon,
+            label: messages.skipBackward,
+          });
+          seekBy(-5);
+          break;
+        case 'l':
+        case 'ArrowRight':
+          e.preventDefault();
+          e.stopPropagation();
+          registerHotkeyEvent(setHotkeyEvents, {
+            icon: Forward5Icon,
+            label: messages.skipForward,
+          });
+          seekBy(5);
+          break;
+        case ',':
+          e.preventDefault();
+          e.stopPropagation();
+          seekBy(-frameTime);
+          break;
+        case '.':
+          e.preventDefault();
+          e.stopPropagation();
+          seekBy(frameTime);
+          break;
+        case 'ArrowUp':
+          e.preventDefault();
+          e.stopPropagation();
+          registerHotkeyEvent(setHotkeyEvents, {
+            icon: VolumeUpIcon,
+            label: messages.volumeUp,
+          });
+          updateVolumeBy(0.15);
+          break;
+        case 'ArrowDown':
+          e.preventDefault();
+          e.stopPropagation();
+          registerHotkeyEvent(setHotkeyEvents, {
+            icon: VolumeDownIcon,
+            label: messages.volumeDown,
+          });
+          updateVolumeBy(-0.15);
+          break;
+      }
+
+      // If we are in fullscreen mode, we don't want any hotkeys
+      // interacting with the UI that's not visible
+
+      if (fullscreen) {
+        e.preventDefault();
+        e.stopPropagation();
+
+        if (e.key === 'Escape') {
+          setHotkeyEvents((events) => [
+            ...events,
+            {
+              key: hotkeyEventId++,
+              icon: FullscreenExitIcon,
+              label: messages.exit_fullscreen,
+            },
+          ]);
+          exitFullscreen();
+        }
+      }
+    },
+    [
+      setHotkeyEvents,
+      togglePlay,
+      toggleFullscreen,
+      toggleMute,
+      fullscreen,
+      frameRate,
+    ],
+  );
+
+  const handleMouseEnter = useCallback(() => {
+    setHovered(true);
+
+    if (hoverTimeoutRef.current) {
+      clearTimeout(hoverTimeoutRef.current);
+    }
+
+    hoverTimeoutRef.current = setTimeout(() => {
+      setHovered(false);
+    }, HOVER_FADE_DELAY);
+  }, [setHovered]);
+
+  const handleMouseMove = useCallback(() => {
+    setHovered(true);
+
+    if (hoverTimeoutRef.current) {
+      clearTimeout(hoverTimeoutRef.current);
+    }
+
+    hoverTimeoutRef.current = setTimeout(() => {
+      setHovered(false);
+    }, HOVER_FADE_DELAY);
+  }, [setHovered]);
+
+  const handleMouseLeave = useCallback(() => {
+    setHovered(false);
+  }, [setHovered]);
+
+  const toggleReveal = useCallback(() => {
+    if (onToggleVisibility) {
+      onToggleVisibility();
+    } else {
+      setRevealed((value) => !value);
+    }
+  }, [setRevealed, onToggleVisibility]);
+
+  const handleLoadedData = useCallback(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    setDuration(videoRef.current.duration);
+
+    if (typeof startTime !== 'undefined') {
+      videoRef.current.currentTime = startTime;
+    }
+
+    if (typeof startVolume !== 'undefined') {
+      videoRef.current.volume = startVolume;
+    }
+
+    if (typeof startMuted !== 'undefined') {
+      videoRef.current.muted = startMuted;
+    }
+
+    if (startPlaying) {
+      void videoRef.current.play();
+    }
+  }, [setDuration, startTime, startVolume, startMuted, startPlaying]);
+
+  const handleProgress = useCallback(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    const lastTimeRange = videoRef.current.buffered.length - 1;
+
+    if (lastTimeRange > -1) {
+      void api.start({
+        buffer: `${Math.ceil(videoRef.current.buffered.end(lastTimeRange) / videoRef.current.duration) * 100}%`,
+        immediate: reduceMotion,
+      });
+    }
+  }, [api]);
+
+  const handleVolumeChange = useCallback(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    setVolume(videoRef.current.volume);
+    setMuted(videoRef.current.muted);
+
+    void api.start({
+      volume: `${videoRef.current.muted ? 0 : videoRef.current.volume * 100}%`,
+      immediate: reduceMotion,
+    });
+
+    persistVolume(videoRef.current.volume, videoRef.current.muted);
+  }, [api, setVolume, setMuted]);
+
+  const handleOpenVideo = useCallback(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    const wasPaused = videoRef.current.paused;
+
+    videoRef.current.pause();
+
+    onOpenVideo?.({
+      startTime: videoRef.current.currentTime,
+      autoPlay: !wasPaused,
+      defaultVolume: videoRef.current.volume,
+    });
+  }, [onOpenVideo]);
+
+  const handleCloseVideo = useCallback(() => {
+    if (!videoRef.current) {
+      return;
+    }
+
+    videoRef.current.pause();
+
+    onCloseVideo?.();
+  }, [onCloseVideo]);
+
+  const handleHotkeyEventDismiss = useCallback(
+    ({ key }: HotkeyEvent) => {
+      setHotkeyEvents((events) => events.filter((e) => e.key !== key));
+    },
+    [setHotkeyEvents],
+  );
+
+  const progress = Math.min((currentTime / duration) * 100, 100);
+  const effectivelyMuted = muted || volume === 0;
+
+  let preload;
+
+  if (startTime || fullscreen || dragging) {
+    preload = 'auto';
+  } else if (detailed) {
+    preload = 'metadata';
+  } else {
+    preload = 'none';
+  }
+
+  // The outer wrapper is necessary to avoid reflowing the layout when going into full screen
+  return (
+    <div>
+      <div
+        role='menuitem'
+        className={classNames('video-player', {
+          inactive: !revealed,
+          detailed,
+          fullscreen,
+          editable,
+        })}
+        style={{ aspectRatio }}
+        ref={playerRef}
+        onMouseEnter={handleMouseEnter}
+        onMouseMove={handleMouseMove}
+        onMouseLeave={handleMouseLeave}
+        onClick={handleClickRoot}
+        onKeyDown={handleKeyDown}
+        tabIndex={0}
+      >
+        {blurhash && (
+          <Blurhash
+            hash={blurhash}
+            className={classNames('media-gallery__preview', {
+              'media-gallery__preview--hidden': revealed,
+            })}
+            dummy={!useBlurhash}
+          />
+        )}
+
+        {(revealed || editable) && (
+          <video /* eslint-disable-line jsx-a11y/media-has-caption */
+            ref={handleVideoRef}
+            src={src}
+            poster={preview}
+            preload={preload}
+            role='button'
+            tabIndex={0}
+            aria-label={alt}
+            title={alt}
+            lang={lang}
+            onClick={handleClick}
+            onKeyDown={handleVideoKeyDown}
+            onPlay={handlePlay}
+            onPause={handlePause}
+            onLoadedData={handleLoadedData}
+            onProgress={handleProgress}
+            onTimeUpdate={handleTimeUpdate}
+            onVolumeChange={handleVolumeChange}
+            style={{ width: '100%' }}
+          />
+        )}
+
+        <HotkeyIndicator
+          events={hotkeyEvents}
+          onDismiss={handleHotkeyEventDismiss}
+        />
+
+        <SpoilerButton
+          hidden={revealed || editable}
+          sensitive={sensitive ?? false}
+          onClick={toggleReveal}
+          matchedFilters={matchedFilters}
+        />
+
+        {!onCloseVideo &&
+          !editable &&
+          !fullscreen &&
+          !alwaysVisible &&
+          revealed && (
+            <div
+              className={classNames('media-gallery__actions', {
+                active: paused || hovered,
+              })}
+            >
+              <button
+                className='media-gallery__actions__pill'
+                onClick={toggleReveal}
+              >
+                <FormattedMessage
+                  id='media_gallery.hide'
+                  defaultMessage='Hide'
+                />
+              </button>
+            </div>
+          )}
+
+        <div
+          className={classNames('video-player__controls', {
+            active: paused || hovered,
+          })}
+        >
+          <div
+            className='video-player__seek'
+            role='slider'
+            aria-valuemin={0}
+            aria-valuenow={progress}
+            aria-valuemax={100}
+            onMouseDown={handleSeekMouseDown}
+            onKeyDown={handleVideoKeyDown}
+            tabIndex={0}
+            ref={seekRef}
+          >
+            <animated.div
+              className='video-player__seek__buffer'
+              style={{ width: style.buffer }}
+            />
+            <animated.div
+              className='video-player__seek__progress'
+              style={{ width: style.progress }}
+            />
+
+            <animated.span
+              className={classNames('video-player__seek__handle', {
+                active: dragging,
+              })}
+              style={{ left: style.progress }}
+            />
+          </div>
+
+          <div className='video-player__buttons-bar'>
+            <div className='video-player__buttons left'>
+              <button
+                type='button'
+                title={intl.formatMessage(
+                  paused ? messages.play : messages.pause,
+                )}
+                aria-label={intl.formatMessage(
+                  paused ? messages.play : messages.pause,
+                )}
+                className='player-button'
+                onClick={togglePlay}
+              >
+                <Icon
+                  id={paused ? 'play' : 'pause'}
+                  icon={paused ? PlayArrowIcon : PauseIcon}
+                />
+              </button>
+              <button
+                type='button'
+                title={intl.formatMessage(
+                  effectivelyMuted ? messages.unmute : messages.mute,
+                )}
+                aria-label={intl.formatMessage(
+                  muted ? messages.unmute : messages.mute,
+                )}
+                className='player-button'
+                onClick={toggleMute}
+              >
+                <Icon
+                  id={effectivelyMuted ? 'volume-off' : 'volume-up'}
+                  icon={effectivelyMuted ? VolumeOffIcon : VolumeUpIcon}
+                />
+              </button>
+
+              <div
+                className={classNames('video-player__volume', {
+                  active: hovered,
+                })}
+                role='slider'
+                aria-valuemin={0}
+                aria-valuenow={effectivelyMuted ? 0 : volume * 100}
+                aria-valuemax={100}
+                onMouseDown={handleVolumeMouseDown}
+                ref={volumeRef}
+                tabIndex={0}
+              >
+                <animated.div
+                  className='video-player__volume__current'
+                  style={{ width: style.volume }}
+                />
+
+                <animated.span
+                  className={classNames('video-player__volume__handle')}
+                  style={{ left: style.volume }}
+                />
+              </div>
+
+              {(detailed || fullscreen) && (
+                <span className='video-player__time'>
+                  <span className='video-player__time-current'>
+                    {formatTime(Math.floor(currentTime))}
+                  </span>
+                  <span className='video-player__time-sep'>/</span>
+                  <span className='video-player__time-total'>
+                    {formatTime(Math.floor(duration))}
+                  </span>
+                </span>
+              )}
+            </div>
+
+            <div className='video-player__buttons right'>
+              {!fullscreen && onOpenVideo && (
+                <button
+                  type='button'
+                  title={intl.formatMessage(messages.expand)}
+                  aria-label={intl.formatMessage(messages.expand)}
+                  className='player-button'
+                  onClick={handleOpenVideo}
+                >
+                  <Icon id='expand' icon={RectangleIcon} />
+                </button>
+              )}
+              {onCloseVideo && (
+                <button
+                  type='button'
+                  title={intl.formatMessage(messages.close)}
+                  aria-label={intl.formatMessage(messages.close)}
+                  className='player-button'
+                  onClick={handleCloseVideo}
+                >
+                  <Icon id='compress' icon={FullscreenExitIcon} />
+                </button>
+              )}
+              <button
+                type='button'
+                title={intl.formatMessage(
+                  fullscreen ? messages.exit_fullscreen : messages.fullscreen,
+                )}
+                aria-label={intl.formatMessage(
+                  fullscreen ? messages.exit_fullscreen : messages.fullscreen,
+                )}
+                className='player-button'
+                onClick={toggleFullscreen}
+              >
+                <Icon
+                  id={fullscreen ? 'compress' : 'arrows-alt'}
+                  icon={fullscreen ? FullscreenExitIcon : FullscreenIcon}
+                />
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default Video;
diff --git a/app/javascript/mastodon/hooks/useAccountId.ts b/app/javascript/mastodon/hooks/useAccountId.ts
new file mode 100644
index 0000000000..1cc819ca59
--- /dev/null
+++ b/app/javascript/mastodon/hooks/useAccountId.ts
@@ -0,0 +1,37 @@
+import { useEffect } from 'react';
+
+import { useParams } from 'react-router';
+
+import { fetchAccount, lookupAccount } from 'mastodon/actions/accounts';
+import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+interface Params {
+  acct?: string;
+  id?: string;
+}
+
+export function useAccountId() {
+  const { acct, id } = useParams<Params>();
+  const accountId = useAppSelector(
+    (state) =>
+      id ??
+      (state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
+  );
+
+  const account = useAppSelector((state) =>
+    accountId ? state.accounts.get(accountId) : undefined,
+  );
+  const isAccount = !!account;
+
+  const dispatch = useAppDispatch();
+  useEffect(() => {
+    if (!accountId) {
+      dispatch(lookupAccount(acct));
+    } else if (!isAccount) {
+      dispatch(fetchAccount(accountId));
+    }
+  }, [dispatch, accountId, acct, isAccount]);
+
+  return accountId;
+}
diff --git a/app/javascript/mastodon/hooks/useAccountVisibility.ts b/app/javascript/mastodon/hooks/useAccountVisibility.ts
new file mode 100644
index 0000000000..55651af5a0
--- /dev/null
+++ b/app/javascript/mastodon/hooks/useAccountVisibility.ts
@@ -0,0 +1,20 @@
+import { getAccountHidden } from 'mastodon/selectors/accounts';
+import { useAppSelector } from 'mastodon/store';
+
+export function useAccountVisibility(accountId?: string) {
+  const blockedBy = useAppSelector(
+    (state) => !!state.relationships.getIn([accountId, 'blocked_by'], false),
+  );
+  const suspended = useAppSelector(
+    (state) => !!state.accounts.getIn([accountId, 'suspended'], false),
+  );
+  const hidden = useAppSelector((state) =>
+    accountId ? Boolean(getAccountHidden(state, accountId)) : false,
+  );
+
+  return {
+    blockedBy,
+    suspended,
+    hidden,
+  };
+}
diff --git a/app/javascript/hooks/useHovering.ts b/app/javascript/mastodon/hooks/useHovering.ts
similarity index 100%
rename from app/javascript/hooks/useHovering.ts
rename to app/javascript/mastodon/hooks/useHovering.ts
diff --git a/app/javascript/hooks/useLinks.ts b/app/javascript/mastodon/hooks/useLinks.ts
similarity index 77%
rename from app/javascript/hooks/useLinks.ts
rename to app/javascript/mastodon/hooks/useLinks.ts
index c99f3f4199..abaa108f6b 100644
--- a/app/javascript/hooks/useLinks.ts
+++ b/app/javascript/mastodon/hooks/useLinks.ts
@@ -14,6 +14,9 @@ const isHashtagClick = (element: HTMLAnchorElement) =>
   element.textContent?.[0] === '#' ||
   element.previousSibling?.textContent?.endsWith('#');
 
+const isFeaturedHashtagClick = (element: HTMLAnchorElement) =>
+  isHashtagClick(element) && element.href.includes('/tagged/');
+
 export const useLinks = () => {
   const history = useHistory();
   const dispatch = useAppDispatch();
@@ -29,6 +32,19 @@ export const useLinks = () => {
     [history],
   );
 
+  const handleFeaturedHashtagClick = useCallback(
+    (element: HTMLAnchorElement) => {
+      const { textContent, href } = element;
+
+      if (!textContent) return;
+
+      const url = new URL(href);
+
+      history.push(url.pathname);
+    },
+    [history],
+  );
+
   const handleMentionClick = useCallback(
     async (element: HTMLAnchorElement) => {
       const result = await dispatch(openURL({ url: element.href }));
@@ -61,12 +77,15 @@ export const useLinks = () => {
       if (isMentionClick(target)) {
         e.preventDefault();
         void handleMentionClick(target);
+      } else if (isFeaturedHashtagClick(target)) {
+        e.preventDefault();
+        handleFeaturedHashtagClick(target);
       } else if (isHashtagClick(target)) {
         e.preventDefault();
         handleHashtagClick(target);
       }
     },
-    [handleMentionClick, handleHashtagClick],
+    [handleMentionClick, handleFeaturedHashtagClick, handleHashtagClick],
   );
 
   return handleClick;
diff --git a/app/javascript/hooks/useRenderSignal.ts b/app/javascript/mastodon/hooks/useRenderSignal.ts
similarity index 100%
rename from app/javascript/hooks/useRenderSignal.ts
rename to app/javascript/mastodon/hooks/useRenderSignal.ts
diff --git a/app/javascript/hooks/useSearchParam.ts b/app/javascript/mastodon/hooks/useSearchParam.ts
similarity index 100%
rename from app/javascript/hooks/useSearchParam.ts
rename to app/javascript/mastodon/hooks/useSearchParam.ts
diff --git a/app/javascript/hooks/useSelectableClick.ts b/app/javascript/mastodon/hooks/useSelectableClick.ts
similarity index 100%
rename from app/javascript/hooks/useSelectableClick.ts
rename to app/javascript/mastodon/hooks/useSelectableClick.ts
diff --git a/app/javascript/hooks/useTimeout.ts b/app/javascript/mastodon/hooks/useTimeout.ts
similarity index 100%
rename from app/javascript/hooks/useTimeout.ts
rename to app/javascript/mastodon/hooks/useTimeout.ts
diff --git a/app/javascript/mastodon/locales/af.json b/app/javascript/mastodon/locales/af.json
index 13cc4d726f..d930785658 100644
--- a/app/javascript/mastodon/locales/af.json
+++ b/app/javascript/mastodon/locales/af.json
@@ -278,7 +278,5 @@
   "tabs_bar.notifications": "Kennisgewings",
   "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}",
   "upload_progress.label": "Uploading…",
-  "video.fullscreen": "Volskerm",
-  "video.mute": "Klank afskakel",
-  "video.unmute": "Klank aanskakel"
+  "video.fullscreen": "Volskerm"
 }
diff --git a/app/javascript/mastodon/locales/an.json b/app/javascript/mastodon/locales/an.json
index 73ff10d70e..49b6f41eac 100644
--- a/app/javascript/mastodon/locales/an.json
+++ b/app/javascript/mastodon/locales/an.json
@@ -25,7 +25,6 @@
   "account.endorse": "Amostrar en perfil",
   "account.featured_tags.last_status_at": "Zaguera publicación lo {date}",
   "account.featured_tags.last_status_never": "Sin publicacions",
-  "account.featured_tags.title": "Etiquetas destacadas de {name}",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
   "account.followers.empty": "Encara no sigue dengún a este usuario.",
@@ -506,8 +505,6 @@
   "video.expand": "Expandir video",
   "video.fullscreen": "Pantalla completa",
   "video.hide": "Amagar video",
-  "video.mute": "Silenciar son",
   "video.pause": "Pausar",
-  "video.play": "Reproducir",
-  "video.unmute": "Deixar de silenciar son"
+  "video.play": "Reproducir"
 }
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 30d7105184..326dd8fbc5 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -29,7 +29,6 @@
   "account.endorse": "أوصِ به على صفحتك الشخصية",
   "account.featured_tags.last_status_at": "آخر منشور في {date}",
   "account.featured_tags.last_status_never": "لا توجد رسائل",
-  "account.featured_tags.title": "وسوم {name} المميَّزة",
   "account.follow": "متابعة",
   "account.follow_back": "تابعه بالمثل",
   "account.followers": "مُتابِعون",
@@ -592,7 +591,6 @@
   "poll_button.remove_poll": "إزالة استطلاع الرأي",
   "privacy.change": "اضبط خصوصية المنشور",
   "privacy.direct.long": "كل من ذُكر في المنشور",
-  "privacy.direct.short": "أشخاص محددون",
   "privacy.private.long": "متابعيك فقط",
   "privacy.private.short": "للمتابِعين",
   "privacy.public.long": "أي شخص على أو خارج ماستدون",
@@ -785,8 +783,6 @@
   "video.expand": "توسيع الفيديو",
   "video.fullscreen": "ملء الشاشة",
   "video.hide": "إخفاء الفيديو",
-  "video.mute": "كتم الصوت",
   "video.pause": "إيقاف مؤقت",
-  "video.play": "تشغيل",
-  "video.unmute": "تشغيل الصوت"
+  "video.play": "تشغيل"
 }
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index 818670742a..5edce9a4d8 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -4,15 +4,16 @@
   "about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
   "about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
-  "about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles ya'l conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
+  "about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles y el conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
   "about.domain_blocks.silenced.title": "Llendóse",
-  "about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai que cualesquier interaición o comunicación colos sos perfiles seya imposible.",
+  "about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai imposible cualesquier interaición o comunicación colos sos perfiles.",
   "about.domain_blocks.suspended.title": "Suspendióse",
   "about.not_available": "Esta información nun ta disponible nesti sirvidor.",
   "about.powered_by": "Una rede social descentralizada que tien la teunoloxía de {mastodon}",
   "about.rules": "Normes del sirvidor",
   "account.account_note_header": "Nota personal",
   "account.add_or_remove_from_list": "Amestar o quitar de les llistes",
+  "account.badges.bot": "Automatizóse",
   "account.badges.group": "Grupu",
   "account.block": "Bloquiar a @{name}",
   "account.block_domain": "Bloquiar el dominiu {domain}",
@@ -25,8 +26,7 @@
   "account.edit_profile": "Editar el perfil",
   "account.enable_notifications": "Avisame cuando @{name} espublice artículos",
   "account.endorse": "Destacar nel perfil",
-  "account.featured_tags.last_status_never": "Nun hai nengún artículu",
-  "account.featured_tags.title": "Etiquetes destacaes de: {name}",
+  "account.featured_tags.last_status_never": "Nun hai nenguna publicación",
   "account.follow": "Siguir",
   "account.follow_back": "Siguir tamién",
   "account.followers": "Siguidores",
@@ -68,8 +68,13 @@
   "alert.unexpected.message": "Prodúxose un error inesperáu.",
   "alert.unexpected.title": "¡Meca!",
   "alt_text_badge.title": "Testu alternativu",
+  "alt_text_modal.add_alt_text": "Amestar testu alternativu",
+  "alt_text_modal.cancel": "Encaboxar",
+  "alt_text_modal.done": "Fecho",
   "announcement.announcement": "Anunciu",
   "annual_report.summary.followers.followers": "siguidores",
+  "annual_report.summary.here_it_is": "Equí ta'l to resume de {year}:",
+  "annual_report.summary.highlighted_post.possessive": "de {name}",
   "annual_report.summary.new_posts.new_posts": "artículos nuevos",
   "annual_report.summary.thanks": "Gracies por ser parte de Mastodon!",
   "attachments_list.unprocessed": "(ensin procesar)",
@@ -87,6 +92,7 @@
   "bundle_column_error.routing.body": "Nun se pudo atopar la páxina solicitada. ¿De xuru que la URL de la barra de direiciones ta bien escrita?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Zarrar",
+  "bundle_modal_error.retry": "Retentar",
   "closed_registrations.other_server_instructions": "Darréu que Mastodon ye una rede social descentralizada, pues crear una cuenta n'otru sirvidor y siguir interactuando con esti.",
   "closed_registrations_modal.description": "Anguaño nun ye posible crear cuentes en {domain}, mas ten en cuenta que nun precises una cuenta nesti sirvidor pa usar Mastodon.",
   "closed_registrations_modal.find_another_server": "Atopar otru sirvidor",
@@ -101,6 +107,7 @@
   "column.domain_blocks": "Dominios bloquiaos",
   "column.edit_list": "Editar la llista",
   "column.favourites": "Favoritos",
+  "column.firehose": "Feed en direuto",
   "column.follow_requests": "Solicitúes de siguimientu",
   "column.home": "Aniciu",
   "column.lists": "Llistes",
@@ -114,14 +121,15 @@
   "column_header.pin": "Fixar",
   "column_header.show_settings": "Amosar la configuración",
   "column_header.unpin": "Lliberar",
+  "column_search.cancel": "Encaboxar",
   "column_subheading.settings": "Configuración",
   "community.column_settings.media_only": "Namás el conteníu multimedia",
   "community.column_settings.remote_only": "Namás lo remoto",
   "compose.language.change": "Camudar la llingua",
   "compose.language.search": "Buscar llingües…",
-  "compose.published.body": "Espublizóse l'artículu.",
+  "compose.published.body": "Publicóse la publicación.",
   "compose.published.open": "Abrir",
-  "compose.saved.body": "Post guardáu.",
+  "compose.saved.body": "Guardóse la publicación.",
   "compose_form.direct_message_warning_learn_more": "Saber más",
   "compose_form.encryption_warning": "Los artículos de Mastodon nun tán cifraos de puntu a puntu. Nun compartas nengún tipu d'información sensible per Mastodon.",
   "compose_form.lock_disclaimer": "La to cuenta nun ye {locked}. Cualesquier perfil pue siguite pa ver los artículos que son namás pa siguidores.",
@@ -129,26 +137,41 @@
   "compose_form.placeholder": "¿En qué pienses?",
   "compose_form.poll.option_placeholder": "Opción {number}",
   "compose_form.poll.type": "Tipu",
-  "compose_form.publish_form": "Artículu nuevu",
+  "compose_form.publish": "Espublizar",
+  "compose_form.publish_form": "Publicación nueva",
+  "compose_form.reply": "Responder",
   "confirmation_modal.cancel": "Encaboxar",
   "confirmations.block.confirm": "Bloquiar",
   "confirmations.delete.confirm": "Desaniciar",
-  "confirmations.delete.message": "¿De xuru que quies desaniciar esti artículu?",
+  "confirmations.delete.message": "¿De xuru que quies desaniciar esta publicación?",
+  "confirmations.delete.title": "¿Quies desaniciar esta publicación?",
   "confirmations.delete_list.confirm": "Desaniciar",
+  "confirmations.delete_list.message": "¿De xuru que quies desaniciar permanentemente esta llista?",
+  "confirmations.delete_list.title": "¿Quies desaniciar la llista?",
   "confirmations.discard_edit_media.confirm": "Escartar",
   "confirmations.edit.confirm": "Editar",
   "confirmations.edit.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?",
+  "confirmations.follow_to_list.title": "¿Siguir al usuariu?",
   "confirmations.logout.confirm": "Zarrar la sesión",
   "confirmations.logout.message": "¿De xuru que quies zarrar la sesión?",
+  "confirmations.logout.title": "¿Quies zarrar la sesión?",
+  "confirmations.missing_alt_text.confirm": "Amestar testu alternativu",
+  "confirmations.missing_alt_text.title": "¿Quies amestar testu alternativu?",
   "confirmations.redraft.confirm": "Desaniciar y reeditar",
+  "confirmations.redraft.title": "¿Desaniciar y reeditar la publicación?",
   "confirmations.reply.confirm": "Responder",
+  "confirmations.reply.message": "Responder agora va sobrescribir el mensaxe que tas componiendo anguaño. ¿De xuru que quies siguir?",
   "confirmations.unfollow.confirm": "Dexar de siguir",
   "confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
+  "confirmations.unfollow.title": "¿Dexar de siguir al usuariu?",
+  "content_warning.hide": "Esconder la publicación",
+  "content_warning.show": "Amosar de toes toes",
   "content_warning.show_more": "Amosar más",
   "conversation.delete": "Desaniciar la conversación",
   "conversation.mark_as_read": "Marcar como lleíu",
   "conversation.open": "Ver la conversación",
   "conversation.with": "Con {names}",
+  "copy_icon_button.copied": "Copiáu nel cartafueyu",
   "copypaste.copied": "Copióse",
   "copypaste.copy_to_clipboard": "Copiar nel cartafueyu",
   "directory.federated": "Del fediversu conocíu",
@@ -159,10 +182,11 @@
   "dismissable_banner.community_timeline": "Esta seición contién los artículos públicos más actuales de los perfiles agospiaos nel dominiu {domain}.",
   "dismissable_banner.dismiss": "Escartar",
   "domain_block_modal.block": "Bloquiar el sirvidor",
+  "domain_block_modal.they_cant_follow": "Naide d'esti sirvidor pue siguite.",
   "domain_block_modal.title": "Bloquiar el dominiu?",
   "domain_pill.server": "Sirvidor",
   "domain_pill.username": "Nome d'usuariu",
-  "embed.instructions": "Empotra esti artículu nel to sitiu web copiando'l códigu d'abaxo.",
+  "embed.instructions": "Empotra esta publicación nel to sitiu web copiando'l códigu d'abaxo.",
   "embed.preview": "Va apaecer asina:",
   "emoji_button.activity": "Actividá",
   "emoji_button.flags": "Banderes",
@@ -176,9 +200,10 @@
   "emoji_button.search_results": "Resultaos de la busca",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viaxes y llugares",
-  "empty_column.account_timeline": "¡Equí nun hai nengún artículu!",
+  "empty_column.account_suspended": "Cuenta suspendida",
+  "empty_column.account_timeline": "¡Equí nun hai nenguna publicación!",
   "empty_column.blocks": "Nun bloquiesti a nengún perfil.",
-  "empty_column.bookmarked_statuses": "Nun tienes nengún artículu en Marcadores. Cuando amiestes dalgún, apaez equí.",
+  "empty_column.bookmarked_statuses": "Nun tienes nenguna publicación en Marcadores. Cuando amiestes dalguna, va apaecer equí.",
   "empty_column.direct": "Nun tienes nenguna mención privada. Cuando unvies o recibas dalguna, apaez equí.",
   "empty_column.domain_blocks": "Nun hai nengún dominiu bloquiáu.",
   "empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!",
@@ -198,20 +223,21 @@
   "explore.trending_links": "Noticies",
   "explore.trending_statuses": "Artículos",
   "explore.trending_tags": "Etiquetes",
-  "filter_modal.added.context_mismatch_explanation": "Esta categoría de peñera nun s'aplica al contestu nel qu'accediesti a esti artículu. Si tamién quies que se peñere l'artículu nesti contestu, tienes d'editar la peñera.",
+  "filter_modal.added.context_mismatch_explanation": "Esta categoría de peñera nun s'aplica al contestu nel qu'accediesti a esta publicación. Si tamién quies que se peñere la publicación nesti contestu, tienes d'editar la peñera.",
   "filter_modal.added.context_mismatch_title": "¡El contestu nun coincide!",
   "filter_modal.added.expired_explanation": "Esta categoría de peñera caducó, tienes de camudar la so data de caducidá p'aplicala.",
   "filter_modal.added.expired_title": "¡La peñera caducó!",
   "filter_modal.added.review_and_configure": "Pa revisar y configurar a fondu esta categoría de peñera, vete a la {settings_link}.",
   "filter_modal.added.review_and_configure_title": "Configuración de la peñera",
   "filter_modal.added.settings_link": "páxina de configuración",
-  "filter_modal.added.short_explanation": "Esti artículu amestóse a la categoría de peñera siguiente: {title}.",
+  "filter_modal.added.short_explanation": "Esta publicación amestóse a la categoría de peñera siguiente: {title}.",
   "filter_modal.added.title": "¡Amestóse la peñera!",
+  "filter_modal.select_filter.expired": "caducó",
   "filter_modal.select_filter.prompt_new": "Categoría nueva: {name}",
   "filter_modal.select_filter.search": "Buscar o crear",
   "filter_modal.select_filter.subtitle": "Usa una categoría esistente o créala",
-  "filter_modal.select_filter.title": "Peñerar esti artículu",
-  "filter_modal.title.status": "Peñera d'un artículu",
+  "filter_modal.select_filter.title": "Peñerar esta publicación",
+  "filter_modal.title.status": "Peñera d'una publicación",
   "firehose.all": "Tolos sirvidores",
   "firehose.local": "Esti sirvidor",
   "firehose.remote": "Otros sirvidores",
@@ -241,6 +267,7 @@
   "hashtag.column_header.tag_mode.any": "o {additional}",
   "hashtag.column_header.tag_mode.none": "ensin {additional}",
   "hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia",
+  "hashtag.column_settings.select.placeholder": "Introduz etiquetes…",
   "hashtag.column_settings.tag_mode.all": "Toes estes",
   "hashtag.column_settings.tag_mode.any": "Cualesquiera d'estes",
   "hashtag.column_settings.tag_mode.none": "Nenguna d'estes",
@@ -252,21 +279,26 @@
   "home.column_settings.show_replies": "Amosar les rempuestes",
   "home.pending_critical_update.body": "¡Anueva'l sirvidor de Mastodon namás que puedas!",
   "home.show_announcements": "Amosar anuncios",
+  "info_button.label": "Ayuda",
+  "interaction_modal.go": "Dir",
+  "interaction_modal.no_account_yet": "¿Tovía nun tienes una cuenta?",
   "interaction_modal.on_another_server": "N'otru sirvidor",
   "interaction_modal.on_this_server": "Nesti sirvidor",
-  "interaction_modal.title.reply": "Rempuesta al artículu de: {name}",
+  "interaction_modal.title.follow": "Siguir a {name}",
+  "interaction_modal.title.reply": "Rempuesta a la publicación de: {name}",
+  "interaction_modal.title.vote": "Vota na encuesta de {name}",
   "intervals.full.days": "{number, plural, one {# día} other {# díes}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
   "intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
   "keyboard_shortcuts.back": "Dir p'atrás",
   "keyboard_shortcuts.blocked": "Abrir la llista de perfiles bloquiaos",
-  "keyboard_shortcuts.boost": "Compartir un artículu",
+  "keyboard_shortcuts.boost": "Compartir una publicación",
   "keyboard_shortcuts.column": "Enfocar una columna",
   "keyboard_shortcuts.compose": "Enfocar l'área de composición",
   "keyboard_shortcuts.description": "Descripción",
   "keyboard_shortcuts.direct": "p'abrir la columna de les menciones privaes",
   "keyboard_shortcuts.down": "Baxar na llista",
-  "keyboard_shortcuts.enter": "Abrir un artículu",
+  "keyboard_shortcuts.enter": "Abrir una publicación",
   "keyboard_shortcuts.federated": "Abrir la llinia de tiempu federada",
   "keyboard_shortcuts.heading": "Atayos del tecláu",
   "keyboard_shortcuts.home": "Abrir la llinia de tiempu del aniciu",
@@ -280,21 +312,26 @@
   "keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia",
   "keyboard_shortcuts.pinned": "Abrir la llista d'artículos fixaos",
   "keyboard_shortcuts.profile": "Abrir el perfil del autor/a",
-  "keyboard_shortcuts.reply": "Responder a un artículu",
+  "keyboard_shortcuts.reply": "Responder a una publicación",
   "keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu",
   "keyboard_shortcuts.search": "Enfocar la barra de busca",
   "keyboard_shortcuts.start": "Abrir la columna «Entamar»",
   "keyboard_shortcuts.toggle_sensitivity": "Amosar/esconder el conteníu multimedia",
-  "keyboard_shortcuts.toot": "Comenzar un artículu nuevu",
+  "keyboard_shortcuts.toot": "Escribir una publicación nueva",
   "keyboard_shortcuts.unfocus": "Desenfocar l'área de composición/busca",
   "keyboard_shortcuts.up": "Xubir na llista",
   "lightbox.close": "Zarrar",
   "lightbox.next": "Siguiente",
   "limited_account_hint.action": "Amosar el perfil de toes toes",
+  "link_preview.author": "Por {name}",
+  "link_preview.more_from_author": "Más de {name}",
+  "lists.add_member": "Amestar",
   "lists.add_to_list": "Amestar a la llista",
   "lists.add_to_lists": "Amestar {name} a la llista",
+  "lists.create": "Crear",
   "lists.create_list": "Crear llista",
   "lists.delete": "Desaniciar la llista",
+  "lists.done": "Fecho",
   "lists.edit": "Editar la llista",
   "lists.list_name": "Nome de la llista",
   "lists.no_lists_yet": "Ensin llistes tovía.",
@@ -332,12 +369,17 @@
   "notification.admin.sign_up": "{name} rexistróse",
   "notification.follow": "{name} siguióte",
   "notification.follow_request": "{name} solicitó siguite",
+  "notification.label.mention": "Mención",
+  "notification.label.private_mention": "Mención privada",
+  "notification.label.private_reply": "Rempuesta privada",
+  "notification.label.reply": "Responder",
+  "notification.mention": "Mención",
   "notification.mentioned_you": "{name} mentóte",
   "notification.moderation-warning.learn_more": "Deprender más",
   "notification.poll": "Finó una encuesta na que votesti",
-  "notification.reblog": "{name} compartió'l to artículu",
+  "notification.reblog": "{name} compartió la to publicación",
   "notification.status": "{name} ta acabante d'espublizar",
-  "notification.update": "{name} editó un artículu",
+  "notification.update": "{name} editó una publicación",
   "notification_requests.edit_selection": "Editar",
   "notification_requests.exit_selection": "Fecho",
   "notifications.clear": "Borrar los avisos",
@@ -364,6 +406,9 @@
   "notifications.group": "{count} avisos",
   "notifications.mark_as_read": "Marcar tolos avisos como lleíos",
   "notifications.permission_required": "Los avisos d'escritoriu nun tán disponibles porque nun se concedió'l permisu riquíu.",
+  "notifications.policy.accept": "Aceptar",
+  "notifications.policy.accept_hint": "Amosar n'avisos",
+  "onboarding.follows.done": "Fecho",
   "onboarding.profile.note": "Biografía",
   "onboarding.profile.note_hint": "Pues @mentar a otros perfiles o poner #etiquetes…",
   "password_confirmation.exceeds_maxlength": "La contraseña de confirmación supera la llongura de caráuteres máxima",
@@ -376,10 +421,10 @@
   "poll.votes": "{votes, plural, one {# votu} other {# votos}}",
   "poll_button.add_poll": "Amestar una encuesta",
   "poll_button.remove_poll": "Quitar la encuesta",
-  "privacy.change": "Configurar la privacidá del artículu",
-  "privacy.direct.short": "Perfiles específicos",
+  "privacy.change": "Configurar la privacidá de la publicación",
+  "privacy.direct.short": "Mención privada",
   "privacy.private.short": "Siguidores",
-  "privacy.public.short": "Artículu públicu",
+  "privacy.public.short": "Publicación pública",
   "privacy_policy.last_updated": "Data del últimu anovamientu: {date}",
   "privacy_policy.title": "Política de privacidá",
   "refresh": "Anovar",
@@ -403,7 +448,7 @@
   "report.category.subtitle": "Escueyi la meyor opción",
   "report.category.title": "Dinos qué pasa con esti {type}",
   "report.category.title_account": "perfil",
-  "report.category.title_status": "artículu",
+  "report.category.title_status": "publicación",
   "report.close": "Fecho",
   "report.comment.title": "¿Hai daqué más qu'habríemos saber?",
   "report.forward": "Reunviar a {target}",
@@ -423,7 +468,7 @@
   "report.rules.subtitle": "Seleiciona tolo que s'axuste",
   "report.rules.title": "¿Qué normes s'incumplen?",
   "report.statuses.subtitle": "Seleiciona tolo que s'axuste",
-  "report.statuses.title": "¿Hai dalgún artículu qu'apoye esti informe?",
+  "report.statuses.title": "¿Hai dalguna publicación qu'apoye esti informe?",
   "report.submit": "Unviar",
   "report.target": "Informe de: {target}",
   "report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:",
@@ -432,7 +477,7 @@
   "report.thanks.title_actionable": "Gracies pol informe, el casu yá ta n'investigación.",
   "report.unfollow": "Dexar de siguir a @{name}",
   "report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.",
-  "report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} artículu} other {Axuntáronse {count} artículos}}",
+  "report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} publicación} other {Axuntáronse {count} publicaciones}}",
   "report_notification.categories.legal": "Llegal",
   "report_notification.categories.legal_sentence": "conteníu illegal",
   "report_notification.categories.spam": "Spam",
@@ -445,6 +490,7 @@
   "search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}",
   "search.quick_action.status_search": "Artículos que concasen con {x}",
   "search.search_or_paste": "Busca o apiega una URL",
+  "search_popout.full_text_search_disabled_message": "Nun ta disponible nel dominiu {domain}.",
   "search_popout.language_code": "códigu de llingua ISO",
   "search_popout.options": "Opciones de busca",
   "search_popout.quick_actions": "Aiciones rápides",
@@ -456,22 +502,25 @@
   "search_results.hashtags": "Etiquetes",
   "search_results.see_all": "Ver too",
   "search_results.statuses": "Artículos",
+  "server_banner.is_one_of_many": "{domain} ye unu de los munchos sirvidores independientes de Mastodon que pues usar pa participar nel fediversu.",
   "server_banner.server_stats": "Estadístiques del sirvidor:",
   "sign_in_banner.create_account": "Crear una cuenta",
+  "sign_in_banner.mastodon_is": "Mastodon ye la meyor manera de siguir al momentu qué pasa.",
+  "sign_in_banner.sign_in": "Aniciar la sesión",
   "sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase",
   "status.admin_account": "Abrir la interfaz de moderación pa @{name}",
   "status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»",
-  "status.admin_status": "Abrir esti artículu na interfaz de moderación",
+  "status.admin_status": "Abrir esta publicación na interfaz de moderación",
   "status.block": "Bloquiar a @{name}",
   "status.bookmark": "Meter en Marcadores",
-  "status.cannot_reblog": "Esti artículu nun se pue compartir",
-  "status.copy": "Copiar l'enllaz al artículu",
+  "status.cannot_reblog": "Esta publicación nun se pue compartir",
+  "status.copy": "Copiar l'enllaz a la publicación",
   "status.delete": "Desaniciar",
   "status.direct": "Mentar a @{name} per privao",
   "status.direct_indicator": "Mención privada",
   "status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}",
   "status.embed": "Consiguir el códigu pa empotrar",
-  "status.filter": "Peñerar esti artículu",
+  "status.filter": "Peñerar esta publicación",
   "status.history.created": "{name} creó {date}",
   "status.history.edited": "{name} editó {date}",
   "status.load_more": "Cargar más",
@@ -480,13 +529,13 @@
   "status.more": "Más",
   "status.mute": "Desactivar los avisos de @{name}",
   "status.mute_conversation": "Desactivar los avisos de la conversación",
-  "status.open": "Espander esti artículu",
+  "status.open": "Espander esta publicación",
   "status.pin": "Fixar nel perfil",
-  "status.pinned": "Artículu fixáu",
+  "status.pinned": "Publicación fixada",
   "status.read_more": "Lleer más",
   "status.reblog": "Compartir",
   "status.reblogged_by": "{name} compartió",
-  "status.reblogs.empty": "Naide nun compartió esti artículu. Cuando daquién lo faiga, apaez equí.",
+  "status.reblogs.empty": "Naide nun compartió esta publicación. Cuando daquién lo faiga, va apaecer equí.",
   "status.redraft": "Desaniciar y reeditar",
   "status.remove_bookmark": "Desaniciar el marcador",
   "status.replied_to": "En rempuesta a {name}",
@@ -527,8 +576,6 @@
   "video.expand": "Espander el videu",
   "video.fullscreen": "Pantalla completa",
   "video.hide": "Esconder el videu",
-  "video.mute": "Desactivar el soníu",
   "video.pause": "Posar",
-  "video.play": "Reproducir",
-  "video.unmute": "Activar el soníu"
+  "video.play": "Reproducir"
 }
diff --git a/app/javascript/mastodon/locales/az.json b/app/javascript/mastodon/locales/az.json
index 5f1efe3d72..550312f31d 100644
--- a/app/javascript/mastodon/locales/az.json
+++ b/app/javascript/mastodon/locales/az.json
@@ -29,7 +29,6 @@
   "account.endorse": "Profildə seçilmişlərə əlavə et",
   "account.featured_tags.last_status_at": "Son paylaşım {date} tarixində olub",
   "account.featured_tags.last_status_never": "Paylaşım yoxdur",
-  "account.featured_tags.title": "{name} istifadəçisinin seçilmiş heşteqləri",
   "account.follow": "İzlə",
   "account.follow_back": "Sən də izlə",
   "account.followers": "İzləyicilər",
@@ -86,6 +85,13 @@
   "alert.unexpected.message": "Bilinməyən bir xəta baş verdi.",
   "alert.unexpected.title": "Ah!",
   "alt_text_badge.title": "Alternativ mətn",
+  "alt_text_modal.add_alt_text": "Alternativ mətn əlavə et",
+  "alt_text_modal.add_text_from_image": "Şəkildəki mətni əlavə et",
+  "alt_text_modal.cancel": "İmtina",
+  "alt_text_modal.change_thumbnail": "Miniatürü dəyişdir",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Eşitmə məhdudiyyətli insanlar üçün bunu izah et…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Görmə məhdudiyyətli insanlar üçün bunu təsvir et…",
+  "alt_text_modal.done": "Oldu",
   "announcement.announcement": "Elan",
   "annual_report.summary.archetype.booster": "Trend ovçusu",
   "annual_report.summary.archetype.lurker": "Lurker",
@@ -211,6 +217,10 @@
   "confirmations.logout.confirm": "Çıxış et",
   "confirmations.logout.message": "Çıxmaq istədiyinizə əminsiniz?",
   "confirmations.logout.title": "Çıxış edilsin?",
+  "confirmations.missing_alt_text.confirm": "Alternativ mətn əlavə et",
+  "confirmations.missing_alt_text.message": "Paylaşımınız alternativ mətn ehtiva etmir. Təsvir əlavə etmək onun daha çox insan üçün əlçatan olmasına kömək edir.",
+  "confirmations.missing_alt_text.secondary": "Yenə də paylaş",
+  "confirmations.missing_alt_text.title": "Alternativ mətn əlavə edilsin?",
   "confirmations.mute.confirm": "Səssizləşdir",
   "confirmations.redraft.confirm": "Sil və qaralamaya köçür",
   "confirmations.redraft.message": "Bu paylaşımı silmək və qaralamaya köçürmək istədiyinizə əminsiniz? Bəyənmələr və gücləndirmələr itəcək və orijinal paylaşıma olan cavablar tənha qalacaq.",
@@ -241,6 +251,62 @@
   "dismissable_banner.dismiss": "Bağla",
   "dismissable_banner.explore_links": "Bu xəbərlər bu gün fediversedə ən çox paylaşılır. Daha fərqli insanlar tərəfindən dərc edilən daha yeni xəbərlər daha yuxarıda sıralanır.",
   "dismissable_banner.explore_statuses": "Fediversedən olan bu paylaşımlar bu gün maraq qazanır. Daha çox gücləndirici və bəyənmə olan daha yeni paylaşımlar daha yuxarıda sıralanır.",
+  "dismissable_banner.explore_tags": "Bu heşteqlər fediverse-də trend olublar. Daha çox fərqli insanlar tərəfindən istifadə olunan heşteqlər daha yuxarıda sıralanır.",
+  "dismissable_banner.public_timeline": "Bunlar, {domain} saytında insanların izlədiyi fediversedəki insanların ən son ictimai paylaşımlarıdır.",
+  "domain_block_modal.block": "Serveri blokla",
   "domain_block_modal.block_account_instead": "@{name} istifadəçisini blokla",
-  "domain_block_modal.they_can_interact_with_old_posts": "Bu serverdən olan insanlar köhnə paylaşımlarınızla əlaqə qura bilər."
+  "domain_block_modal.they_can_interact_with_old_posts": "Bu serverdən olan insanlar köhnə paylaşımlarınızla əlaqə qura bilər.",
+  "domain_block_modal.they_cant_follow": "Bu serverdən heç kim sizi izləyə bilməz.",
+  "domain_block_modal.they_wont_know": "Onlar bloklandıqlarını bilməyəcəklər.",
+  "domain_block_modal.title": "Domen bloklansın?",
+  "domain_block_modal.you_will_lose_num_followers": "Siz {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} izləyici}} və izlədiyiniz {followingCount, plural, one {{followingCountDisplay} istifadəçini} other {{followingCountDisplay} istifadəçini}} itirəcəksiniz.",
+  "domain_block_modal.you_will_lose_relationships": "Bu serverdən olan bütün izləyicilərinizi və izlədiklərinizi itirəcəksiniz.",
+  "domain_block_modal.you_wont_see_posts": "Bu serverdən olan paylaşımları və istifadəçilərdən olan bildirişləri görməyəcəksiniz.",
+  "domain_pill.activitypub_lets_connect": "Bu, təkcə Mastodonda deyil, həm də müxtəlif sosial tətbiqlərdə insanlarla əlaqə saxlamağa və onlarla ünsiyyət qurmağa imkan verir.",
+  "domain_pill.activitypub_like_language": "ActivityPub-ı Mastodonun digər sosial şəbəkələrlə danışdığı dil kimi düşünə bilərsiniz.",
+  "domain_pill.server": "Server",
+  "domain_pill.their_handle": "Tanıdıcısı:",
+  "domain_pill.their_server": "Onların bütün paylaşımlarının yaşadığı rəqəmsal ev.",
+  "domain_pill.their_username": "Serverdəki unikal identifikator. Fərqli serverlərdə eyni istifadəçi adı ilə istifadəçilər tapmaq mümkündür.",
+  "domain_pill.username": "İstifadəçi adı",
+  "domain_pill.whats_in_a_handle": "Tanıdıcı nədir?",
+  "domain_pill.who_they_are": "Tanıdıcılar kimin kim olduğunu və harada olduğunu bildirdiyi üçün siz <button>ActivityPub tərəfindən dəstəklənən platformaların</button> sosial şəbəkəsindəki bütün insanlarla əlaqə saxlaya bilərsiniz.",
+  "domain_pill.who_you_are": "Tanıdıcılar sizin kim olduğunuzu və harada olduğunuzu bildirdiyi üçün <button>ActivityPub tərəfindən dəstəklənən platformaların</button> sosial şəbəkəsindəki bütün insanlar sizlə əlaqə saxlaya bilər.",
+  "domain_pill.your_handle": "Tanıdıcınız:",
+  "domain_pill.your_server": "Bütün paylaşımlarınızın yaşadığı rəqəmsal ev. Buranı bəyənmirsiniz? İstədiyiniz vaxt serverdən köçün və izləyicilərinizi də aparın.",
+  "domain_pill.your_username": "Serverdəki unikal identifikatoruz. Fərqli serverlərdə eyni istifadəçi adı ilə istifadəçilər tapmaq mümkündür.",
+  "embed.instructions": "Aşağıdakı kodu kopyalayaraq bu postu veb-saytınıza yerləşdirin.",
+  "embed.preview": "Belə görünəcək:",
+  "emoji_button.activity": "Aktivlik",
+  "emoji_button.clear": "Təmizlə",
+  "emoji_button.custom": "Özəl",
+  "emoji_button.flags": "Bayraqlar",
+  "emoji_button.food": "Yemək və içki",
+  "emoji_button.label": "Emoji daxil et",
+  "emoji_button.nature": "Təbiət",
+  "emoji_button.not_found": "Uyğun emoji tapılmadı",
+  "emoji_button.objects": "Obyektlər",
+  "emoji_button.people": "İnsanlar",
+  "emoji_button.recent": "Tez-tez istifadə edilən",
+  "emoji_button.search": "Axtar...",
+  "emoji_button.search_results": "Axtarış nəticələri",
+  "emoji_button.symbols": "Simvollar",
+  "emoji_button.travel": "Səyahət və məkanlar",
+  "empty_column.account_hides_collections": "Bu istifadəçi bu məlumatı əlçatan etməməyi seçib",
+  "empty_column.account_suspended": "Hesab silinib",
+  "empty_column.account_timeline": "Heç bir paylaşım yoxdur!",
+  "empty_column.account_unavailable": "Profil əlçatan deyil",
+  "empty_column.blocks": "Hələ ki, heç bir istifadəçini bloklamamasınız.",
+  "empty_column.bookmarked_statuses": "Hələ ki, heç bir paylaşımı yadda saxlamamısınız. Yadda saxlayanda burada görünəcək.",
+  "empty_column.community": "Lokal zaman qrafiki boşdur. Topun yuvarlanmağa başlaması üçün ictimai bir şey paylaşın!",
+  "empty_column.direct": "Gizli etiketiniz yoxdur. Göndərdikdə və ya qəbul etdikdə burada görəcəksiniz.",
+  "empty_column.domain_blocks": "Hələ ki, bloklanmış domen yoxdur.",
+  "empty_column.explore_statuses": "Hal-hazırda trenddə heç yoxdur. Daha sonra yenidən yoxlayın!",
+  "empty_column.favourited_statuses": "Bəyəndiyiniz paylaşımlar yoxdur. Birini bəyəndikdə burada görünəcək.",
+  "empty_column.favourites": "Bu paylaşımı hələ ki, heç kim bəyənməyib. Bəyənildikdə burada görünəcək.",
+  "empty_column.follow_requests": "İzləmə sorğularınız yoxdur. Qəbul etdikdə burada görəcəksiniz.",
+  "empty_column.followed_tags": "Heç bir heşteq izləmirsiniz. İzlədikdə burada görünəcək.",
+  "empty_column.hashtag": "Bu heşteqdə hələ ki, heç nə yoxdur.",
+  "follow_suggestions.hints.friends_of_friends": "Bu profil izlədiyiniz insanlar arasında populyardır.",
+  "follow_suggestions.hints.most_followed": "Bu profil {domain} serverində ən çox izlənilənlərdən biridir."
 }
diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json
index c68ff38a7c..9011fdfd63 100644
--- a/app/javascript/mastodon/locales/be.json
+++ b/app/javascript/mastodon/locales/be.json
@@ -29,7 +29,6 @@
   "account.endorse": "Паказваць у профілі",
   "account.featured_tags.last_status_at": "Апошні допіс ад {date}",
   "account.featured_tags.last_status_never": "Няма допісаў",
-  "account.featured_tags.title": "Тэгі, выбраныя {name}",
   "account.follow": "Падпісацца",
   "account.follow_back": "Падпісацца ў адказ",
   "account.followers": "Падпісчыкі",
@@ -86,6 +85,7 @@
   "alert.unexpected.message": "Узнікла нечаканая памылка.",
   "alert.unexpected.title": "Вой!",
   "alt_text_badge.title": "Альтэрнатыўны тэкст",
+  "alt_text_modal.done": "Гатова",
   "announcement.announcement": "Аб'ява",
   "annual_report.summary.archetype.booster": "Трэнда-сьледнік",
   "annual_report.summary.archetype.lurker": "Назіральнік",
@@ -211,6 +211,7 @@
   "confirmations.logout.confirm": "Выйсці",
   "confirmations.logout.message": "Вы ўпэўненыя, што хочаце выйсці?",
   "confirmations.logout.title": "Выйсці?",
+  "confirmations.missing_alt_text.title": "Дадаць апісаньне?",
   "confirmations.mute.confirm": "Ігнараваць",
   "confirmations.redraft.confirm": "Выдаліць і перапісаць",
   "confirmations.redraft.message": "Вы ўпэўнены, што хочаце выдаліць допіс і перапісаць яго? Упадабанні і пашырэнні згубяцца, а адказы да арыгінальнага допісу асірацеюць.",
@@ -407,6 +408,7 @@
   "ignore_notifications_modal.not_followers_title": "Ігнараваць паведамленьні ад людзей, якія ня падпісаныя на вас?",
   "ignore_notifications_modal.not_following_title": "Ігнараваць апавяшчэнні ад людзей на якіх вы не падпісаны?",
   "ignore_notifications_modal.private_mentions_title": "Ігнараваць паведамленьні аб непажаданых прыватных згадках?",
+  "info_button.label": "Даведка",
   "interaction_modal.action.favourite": "Каб працягнуць, вы мусіце ўпадабаць з вашага ўліковага запісу.",
   "interaction_modal.action.follow": "Каб працягнуць, вы мусіце падпісацца з вашага ўліковага запісу.",
   "interaction_modal.on_another_server": "На іншым серверы",
@@ -501,6 +503,7 @@
   "navigation_bar.follows_and_followers": "Падпіскі і падпісчыкі",
   "navigation_bar.lists": "Спісы",
   "navigation_bar.logout": "Выйсці",
+  "navigation_bar.moderation": "Мадэрацыя",
   "navigation_bar.mutes": "Ігнараваныя карыстальнікі",
   "navigation_bar.opened_in_classic_interface": "Допісы, уліковыя запісы і іншыя спецыфічныя старонкі па змоўчанні адчыняюцца ў класічным вэб-інтэрфейсе.",
   "navigation_bar.personal": "Асабістае",
@@ -639,7 +642,6 @@
   "poll_button.remove_poll": "Выдаліць апытанне",
   "privacy.change": "Змяніць прыватнасць допісу",
   "privacy.direct.long": "Усе згаданыя ў допісе",
-  "privacy.direct.short": "Канкрэтныя людзі",
   "privacy.private.long": "Толькі вашыя падпісчыкі",
   "privacy.private.short": "Падпісчыкі",
   "privacy.public.long": "Усе, хто ёсць і каго няма ў Mastodon",
@@ -839,8 +841,6 @@
   "video.expand": "Разгарнуць відэа",
   "video.fullscreen": "Увесь экран",
   "video.hide": "Схаваць відэа",
-  "video.mute": "Адключыць гук",
   "video.pause": "Паўза",
-  "video.play": "Прайграць",
-  "video.unmute": "Уключыць гук"
+  "video.play": "Прайграць"
 }
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index cdcbddf55a..2e4c8593d4 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Редактиране на профила",
   "account.enable_notifications": "Известяване при публикуване от @{name}",
   "account.endorse": "Представи в профила",
+  "account.featured": "Препоръчано",
+  "account.featured.hashtags": "Хаштагове",
+  "account.featured.posts": "Публикации",
   "account.featured_tags.last_status_at": "Последна публикация на {date}",
   "account.featured_tags.last_status_never": "Няма публикации",
-  "account.featured_tags.title": "Главни хаштагове на {name}",
   "account.follow": "Последване",
   "account.follow_back": "Последване взаимно",
   "account.followers": "Последователи",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",
   "account.unblock": "Отблокиране на @{name}",
   "account.unblock_domain": "Отблокиране на домейн {domain}",
+  "account.unblock_domain_short": "Отблокиране",
   "account.unblock_short": "Отблокиране",
   "account.unendorse": "Не включвайте в профила",
   "account.unfollow": "Стоп на следването",
@@ -218,6 +221,10 @@
   "confirmations.logout.confirm": "Излизане",
   "confirmations.logout.message": "Наистина ли искате да излезете?",
   "confirmations.logout.title": "Излизате ли от системата?",
+  "confirmations.missing_alt_text.confirm": "Добавяне на алтернативен текст",
+  "confirmations.missing_alt_text.message": "Вашата публикация съдържа мултимедия без алтернативен текст. Добавянето на описание помага да направите съдържанието си по-достъпно за повече хора.",
+  "confirmations.missing_alt_text.secondary": "Все пак да се публикува",
+  "confirmations.missing_alt_text.title": "Добавяте ли алтернативен текст?",
   "confirmations.mute.confirm": "Заглушаване",
   "confirmations.redraft.confirm": "Изтриване и преработване",
   "confirmations.redraft.message": "Наистина ли искате да изтриете тази публикация и да я направите чернова? Означаванията като любими и подсилванията ще се изгубят, а и отговорите към първоначалната публикация ще осиротеят.",
@@ -289,6 +296,7 @@
   "emoji_button.search_results": "Резултати от търсене",
   "emoji_button.symbols": "Символи",
   "emoji_button.travel": "Пътуване и места",
+  "empty_column.account_featured": "Списъкът е празен",
   "empty_column.account_hides_collections": "Този потребител е избрал да не дава тази информация",
   "empty_column.account_suspended": "Спрян акаунт",
   "empty_column.account_timeline": "Тук няма публикации!",
@@ -373,6 +381,8 @@
   "generic.saved": "Запазено",
   "getting_started.heading": "Първи стъпки",
   "hashtag.admin_moderation": "Отваряне на модериращия интерфейс за #{name}",
+  "hashtag.browse": "Разглеждане на публикации в #{hashtag}",
+  "hashtag.browse_from_account": "Разглеждане на публикации от @{name} из #{hashtag}",
   "hashtag.column_header.tag_mode.all": "и {additional}",
   "hashtag.column_header.tag_mode.any": "или {additional}",
   "hashtag.column_header.tag_mode.none": "без {additional}",
@@ -386,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} публикация} other {{counter} публикации}} днес",
   "hashtag.follow": "Следване на хаштаг",
+  "hashtag.mute": "Заглушаване на #{hashtag}",
   "hashtag.unfollow": "Спиране на следване на хаштаг",
   "hashtags.and_other": "…и {count, plural, other {# още}}",
   "hints.profiles.followers_may_be_missing": "Последователи за този профил може да липсват.",
@@ -670,7 +681,7 @@
   "onboarding.follows.title": "Последвайте хора, за да започнете",
   "onboarding.profile.discoverable": "Правене на моя профил откриваем",
   "onboarding.profile.discoverable_hint": "Включвайки откриваемостта в Mastodon, вашите публикации може да се появят при резултатите от търсене и изгряващи неща, и вашия профил може да бъде предложен на хора с подобни интереси като вашите.",
-  "onboarding.profile.display_name": "Името на показ",
+  "onboarding.profile.display_name": "Показвано име",
   "onboarding.profile.display_name_hint": "Вашето пълно име или псевдоним…",
   "onboarding.profile.note": "Биография",
   "onboarding.profile.note_hint": "Може да @споменавате други хора или #хаштагове…",
@@ -693,7 +704,7 @@
   "poll_button.remove_poll": "Премахване на анкета",
   "privacy.change": "Промяна на поверителността на публикация",
   "privacy.direct.long": "Споменатите в публикацията",
-  "privacy.direct.short": "Определени хора",
+  "privacy.direct.short": "Частно споменаване",
   "privacy.private.long": "Само последователите ви",
   "privacy.private.short": "Последователи",
   "privacy.public.long": "Всеки във и извън Mastodon",
@@ -868,7 +879,9 @@
   "subscribed_languages.target": "Промяна на абонираните езици за {target}",
   "tabs_bar.home": "Начало",
   "tabs_bar.notifications": "Известия",
+  "terms_of_service.effective_as_of": "В сила от {date}",
   "terms_of_service.title": "Условия на услугата",
+  "terms_of_service.upcoming_changes_on": "Предстоящи промени на {date}",
   "time_remaining.days": "{number, plural, one {остава # ден} other {остават # дни}}",
   "time_remaining.hours": "{number, plural, one {остава # час} other {остават # часа}}",
   "time_remaining.minutes": "{number, plural, one {остава # минута} other {остават # минути}}",
@@ -899,8 +912,12 @@
   "video.expand": "Разгъване на видеото",
   "video.fullscreen": "Цял екран",
   "video.hide": "Скриване на видеото",
-  "video.mute": "Обеззвучаване",
+  "video.mute": "Заглушаване",
   "video.pause": "Пауза",
   "video.play": "Пускане",
-  "video.unmute": "Включване на звука"
+  "video.skip_backward": "Прескок назад",
+  "video.skip_forward": "Прескок напред",
+  "video.unmute": "Без заглушаване",
+  "video.volume_down": "Намаляване на звука",
+  "video.volume_up": "Увеличаване на звука"
 }
diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json
index d90934b6c9..ec0f4eb447 100644
--- a/app/javascript/mastodon/locales/bn.json
+++ b/app/javascript/mastodon/locales/bn.json
@@ -11,6 +11,7 @@
   "about.not_available": "এই তথ্য এই সার্ভারে উন্মুক্ত করা হয়নি.",
   "about.powered_by": "{mastodon} দ্বারা তৈরি বিকেন্দ্রীভূত সামাজিক মিডিয়া।",
   "about.rules": "সার্ভারের নিয়মাবলী",
+  "account.account_note_header": "ব্যক্তিগত টীকা",
   "account.add_or_remove_from_list": "তালিকাতে যোগ বা অপসারণ করো",
   "account.badges.bot": "বট",
   "account.badges.group": "দল",
@@ -19,6 +20,7 @@
   "account.block_short": "ব্লক",
   "account.blocked": "অবরুদ্ধ",
   "account.cancel_follow_request": "অনুসরণ অনুরোধ প্রত্যাহার করুন",
+  "account.copy": "অবতারের সংযোগ অনুলিপি করো",
   "account.direct": "গোপনে মেনশন করুন @{name}",
   "account.disable_notifications": "আমাকে জানানো বন্ধ করো যখন @{name} পোস্ট করবে",
   "account.domain_blocked": "ডোমেইন ব্লক করা",
@@ -27,8 +29,8 @@
   "account.endorse": "প্রোফাইলে ফিচার করুন",
   "account.featured_tags.last_status_at": "{date} এ সর্বশেষ পোস্ট",
   "account.featured_tags.last_status_never": "কোনো পোস্ট নেই",
-  "account.featured_tags.title": "{name} এর ফিচার করা Hashtag সমূহ",
   "account.follow": "অনুসরণ",
+  "account.follow_back": "তাকে অনুসরণ করো",
   "account.followers": "অনুসরণকারী",
   "account.followers.empty": "এই ব্যক্তিকে এখনো কেউ অনুসরণ করে না.",
   "account.following": "অনুসরণ করা হচ্ছে",
@@ -433,8 +435,6 @@
   "video.expand": "ভিডিওটি বড়ো করতে",
   "video.fullscreen": "পূর্ণ পর্দা করতে",
   "video.hide": "ভিডিওটি লুকাতে",
-  "video.mute": "শব্দ বন্ধ করতে",
   "video.pause": "থামাতে",
-  "video.play": "শুরু করতে",
-  "video.unmute": "শব্দ চালু করতে"
+  "video.play": "শুরু করতে"
 }
diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json
index f551cd5654..51e3723d19 100644
--- a/app/javascript/mastodon/locales/br.json
+++ b/app/javascript/mastodon/locales/br.json
@@ -28,7 +28,6 @@
   "account.endorse": "Lakaat war-wel war ar profil",
   "account.featured_tags.last_status_at": "Toud diwezhañ : {date}",
   "account.featured_tags.last_status_never": "Embannadur ebet",
-  "account.featured_tags.title": "Hashtagoù pennañ {name}",
   "account.follow": "Heuliañ",
   "account.follow_back": "Heuliañ d'ho tro",
   "account.followers": "Tud koumanantet",
@@ -81,9 +80,14 @@
   "alert.rate_limited.title": "Feur bevennet",
   "alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.",
   "alert.unexpected.title": "Hopala !",
+  "alt_text_modal.cancel": "Nullañ",
+  "alt_text_modal.change_thumbnail": "Kemmañ ar velvenn",
+  "alt_text_modal.done": "Graet",
   "announcement.announcement": "Kemennad",
   "annual_report.summary.followers.followers": "heulier",
   "annual_report.summary.highlighted_post.possessive": "{name}",
+  "annual_report.summary.most_used_hashtag.none": "Hini ebet",
+  "annual_report.summary.new_posts.new_posts": "toudoù nevez",
   "attachments_list.unprocessed": "(ket meret)",
   "audio.hide": "Kuzhat ar c'hleved",
   "block_modal.show_less": "Diskouez nebeutoc'h",
@@ -109,9 +113,11 @@
   "column.blocks": "Implijer·ezed·ien berzet",
   "column.bookmarks": "Sinedoù",
   "column.community": "Red-amzer lec'hel",
+  "column.create_list": "Krouiñ ul listenn",
   "column.direct": "Menegoù prevez",
   "column.directory": "Mont a-dreuz ar profiloù",
   "column.domain_blocks": "Domani berzet",
+  "column.edit_list": "Kemmañ al listenn",
   "column.favourites": "Muiañ-karet",
   "column.firehose": "Redoù war-eeun",
   "column.follow_requests": "Rekedoù heuliañ",
@@ -162,9 +168,12 @@
   "confirmations.delete.message": "Ha sur oc'h e fell deoc'h dilemel an toud-mañ ?",
   "confirmations.delete_list.confirm": "Dilemel",
   "confirmations.delete_list.message": "Ha sur eo hoc'h eus c'hoant da zilemel ar roll-mañ da vat ?",
+  "confirmations.delete_list.title": "Dilemel al listenn?",
   "confirmations.discard_edit_media.confirm": "Nac'hañ",
   "confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?",
   "confirmations.edit.confirm": "Kemmañ",
+  "confirmations.edit.message": "Kemmañ bremañ a zilamo ar gemennadenn emaoc'h o skrivañ. Sur e oc'h e fell deoc'h kenderc'hel ganti?",
+  "confirmations.follow_to_list.title": "Heuliañ an implijer·ez?",
   "confirmations.logout.confirm": "Digevreañ",
   "confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?",
   "confirmations.mute.confirm": "Kuzhat",
@@ -266,8 +275,10 @@
   "footer.privacy_policy": "Reolennoù prevezded",
   "footer.source_code": "Gwelet ar c'hod mammenn",
   "footer.status": "Statud",
+  "footer.terms_of_service": "Divizoù implijout hollek",
   "generic.saved": "Enrollet",
   "getting_started.heading": "Loc'hañ",
+  "hashtag.admin_moderation": "Digeriñ an etrefas evezhiañ evit #{name}",
   "hashtag.column_header.tag_mode.all": "ha(g) {additional}",
   "hashtag.column_header.tag_mode.any": "pe {additional}",
   "hashtag.column_header.tag_mode.none": "hep {additional}",
@@ -337,8 +348,14 @@
   "limited_account_hint.action": "Diskouez an aelad memes tra",
   "limited_account_hint.title": "Kuzhet eo bet ar profil-mañ gant an evezhierien eus {domain}.",
   "link_preview.author": "Gant {name}",
+  "lists.add_member": "Ouzhpennañ",
+  "lists.add_to_list": "Ouzhpennañ d'al listenn",
+  "lists.create": "Krouiñ",
+  "lists.create_list": "Krouiñ ul listenn",
   "lists.delete": "Dilemel al listenn",
+  "lists.done": "Graet",
   "lists.edit": "Kemmañ al listenn",
+  "lists.list_name": "Anv al listenn",
   "lists.replies_policy.followed": "Pep implijer.ez heuliet",
   "lists.replies_policy.list": "Izili ar roll",
   "lists.replies_policy.none": "Den ebet",
@@ -373,11 +390,17 @@
   "notification.follow": "heuliañ a ra {name} ac'hanoc'h",
   "notification.follow.name_and_others": "{name} <a>{count, plural, one {hag # den all} two {ha # zen all} few {ha # den all} many {ha # den all} other {ha # den all}}</a> zo o heuliañ ac'hanoc'h",
   "notification.follow_request": "Gant {name} eo bet goulennet ho heuliañ",
+  "notification.label.reply": "Respont",
   "notification.moderation-warning.learn_more": "Gouzout hiroc'h",
   "notification.own_poll": "Echu eo ho sontadeg",
   "notification.reblog": "Gant {name} eo bet skignet ho toud",
+  "notification.relationships_severance_event.learn_more": "Gouzout hiroc'h",
   "notification.status": "Emañ {name} o paouez toudañ",
   "notification.update": "Gant {name} ez eus bet kemmet un toud",
+  "notification_requests.accept": "Asantiñ",
+  "notification_requests.dismiss": "Diverkañ",
+  "notification_requests.edit_selection": "Kemmañ",
+  "notification_requests.exit_selection": "Graet",
   "notifications.clear": "Skarzhañ ar c'hemennoù",
   "notifications.clear_confirmation": "Ha sur oc'h e fell deoc'h skarzhañ ho holl kemennoù ?",
   "notifications.column_settings.admin.report": "Disklêriadurioù nevez :",
@@ -410,6 +433,10 @@
   "notifications.permission_denied": "Kemennoù war ar burev n'int ket hegerz rak pedadenn aotren ar merdeer a zo bet nullet araok",
   "notifications.permission_denied_alert": "Kemennoù wa ar burev na c'hellont ket bezañ lezelet, rak aotre ar merdeer a zo bet nac'het a-raok",
   "notifications.permission_required": "Kemennoù war ar burev n'int ket hegerz abalamour d'an aotre rekis n'eo ket bet roet.",
+  "notifications.policy.accept": "Asantiñ",
+  "notifications.policy.accept_hint": "Diskouez er c’hemennoù",
+  "notifications.policy.drop": "Tremen e-bioù",
+  "notifications.policy.filter": "Silañ",
   "notifications.policy.filter_new_accounts_title": "Kontoù nevez",
   "notifications_permission_banner.enable": "Lezel kemennoù war ar burev",
   "notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.",
@@ -436,7 +463,6 @@
   "poll_button.add_poll": "Ouzhpennañ ur sontadeg",
   "poll_button.remove_poll": "Dilemel ar sontadeg",
   "privacy.change": "Cheñch prevezded an embannadur",
-  "privacy.direct.short": "Tud resis",
   "privacy.private.short": "Heulierien",
   "privacy.public.short": "Publik",
   "privacy_policy.last_updated": "Hizivadenn ziwezhañ {date}",
@@ -516,6 +542,7 @@
   "search_results.accounts": "Profiloù",
   "search_results.all": "Pep tra",
   "search_results.hashtags": "Hashtagoù",
+  "search_results.no_results": "Disoc'h ebet.",
   "search_results.see_all": "Gwelet pep tra",
   "search_results.statuses": "Toudoù",
   "server_banner.active_users": "implijerien·ezed oberiant",
@@ -580,6 +607,7 @@
   "subscribed_languages.target": "Cheñch ar yezhoù koumanantet evit {target}",
   "tabs_bar.home": "Degemer",
   "tabs_bar.notifications": "Kemennoù",
+  "terms_of_service.title": "Divizoù implijout",
   "time_remaining.days": "{number, plural,one {# devezh} other {# a zevezh}} a chom",
   "time_remaining.hours": "{number, plural, one {# eurvezh} other{# eurvezh}} a chom",
   "time_remaining.minutes": "{number, plural, one {# munut} other{# a vunut}} a chom",
@@ -605,8 +633,6 @@
   "video.expand": "Ledanaat ar video",
   "video.fullscreen": "Skramm a-bezh",
   "video.hide": "Kuzhat ar video",
-  "video.mute": "Paouez gant ar son",
   "video.pause": "Paouez",
-  "video.play": "Lenn",
-  "video.unmute": "Lakaat ar son en-dro"
+  "video.play": "Lenn"
 }
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 74d33c2357..60e3066d0e 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Edita el perfil",
   "account.enable_notifications": "Notifica'm els tuts de @{name}",
   "account.endorse": "Recomana en el perfil",
+  "account.featured": "Destacat",
+  "account.featured.hashtags": "Etiquetes",
+  "account.featured.posts": "Publicacions",
   "account.featured_tags.last_status_at": "Darrer tut el {date}",
   "account.featured_tags.last_status_never": "No hi ha tuts",
-  "account.featured_tags.title": "etiquetes destacades de {name}",
   "account.follow": "Segueix",
   "account.follow_back": "Segueix tu també",
   "account.followers": "Seguidors",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} publicació} other {{counter} publicacions}}",
   "account.unblock": "Desbloca @{name}",
   "account.unblock_domain": "Desbloca el domini {domain}",
+  "account.unblock_domain_short": "Desbloca",
   "account.unblock_short": "Desbloca",
   "account.unendorse": "No recomanis en el perfil",
   "account.unfollow": "Deixa de seguir",
@@ -180,7 +183,7 @@
   "compose.published.open": "Obre",
   "compose.saved.body": "Tut desat.",
   "compose_form.direct_message_warning_learn_more": "Més informació",
-  "compose_form.encryption_warning": "Les publicacions a Mastodon no estant xifrades punt a punt. No comparteixis informació sensible mitjançant Mastodon.",
+  "compose_form.encryption_warning": "Els tuts a Mastodon no estan xifrats punt a punt. No compartiu informació confidencial mitjançant Mastodon.",
   "compose_form.hashtag_warning": "Aquest tut no apareixerà a les llistes d'etiquetes perquè no és públic. Només els tuts públics apareixen a les cerques per etiqueta.",
   "compose_form.lock_disclaimer": "El teu compte no està {locked}. Tothom pot seguir-te i veure els tuts de només per a seguidors.",
   "compose_form.lock_disclaimer.lock": "blocat",
@@ -218,7 +221,7 @@
   "confirmations.logout.confirm": "Tanca la sessió",
   "confirmations.logout.message": "Segur que vols tancar la sessió?",
   "confirmations.logout.title": "Tancar la sessió?",
-  "confirmations.missing_alt_text.confirm": "Afegiu text alternatiu",
+  "confirmations.missing_alt_text.confirm": "Afegiu un text alternatiu",
   "confirmations.missing_alt_text.message": "La vostra publicació té contingut sense text alternatiu. Afegir-hi descripcions la farà accessible a més persones.",
   "confirmations.missing_alt_text.secondary": "Publica-la igualment",
   "confirmations.missing_alt_text.title": "Hi voleu afegir text alternatiu?",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Resultats de la cerca",
   "emoji_button.symbols": "Símbols",
   "emoji_button.travel": "Viatges i llocs",
+  "empty_column.account_featured": "Aquesta llista està buida",
   "empty_column.account_hides_collections": "Aquest usuari ha decidit no mostrar aquesta informació",
   "empty_column.account_suspended": "Compte suspès",
   "empty_column.account_timeline": "No hi ha tuts aquí!",
@@ -390,6 +394,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} tut} other {{counter} tuts}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} tut} other {{counter} tuts}} avui",
   "hashtag.follow": "Segueix l'etiqueta",
+  "hashtag.mute": "Silencia #{hashtag}",
   "hashtag.unfollow": "Deixa de seguir l'etiqueta",
   "hashtags.and_other": "…i {count, plural, other {# més}}",
   "hints.profiles.followers_may_be_missing": "Es poden haver perdut seguidors d'aquest perfil.",
@@ -696,7 +701,7 @@
   "poll_button.remove_poll": "Elimina l'enquesta",
   "privacy.change": "Canvia la privacitat del tut",
   "privacy.direct.long": "Tothom mencionat a la publicació",
-  "privacy.direct.short": "Persones concretes",
+  "privacy.direct.short": "Menció privada",
   "privacy.private.long": "Només els vostres seguidors",
   "privacy.private.short": "Seguidors",
   "privacy.public.long": "Tothom dins o fora Mastodon",
@@ -871,7 +876,9 @@
   "subscribed_languages.target": "Canvia les llengües subscrites per a {target}",
   "tabs_bar.home": "Inici",
   "tabs_bar.notifications": "Notificacions",
+  "terms_of_service.effective_as_of": "En vigor a partir de {date}",
   "terms_of_service.title": "Condicions de servei",
+  "terms_of_service.upcoming_changes_on": "Propers canvis el {date}",
   "time_remaining.days": "{number, plural, one {# dia restant} other {# dies restants}}",
   "time_remaining.hours": "{number, plural, one {# hora restant} other {# hores restants}}",
   "time_remaining.minutes": "{number, plural, one {# minut restant} other {# minuts restants}}",
@@ -905,5 +912,9 @@
   "video.mute": "Silencia",
   "video.pause": "Pausa",
   "video.play": "Reprodueix",
-  "video.unmute": "Activa el so"
+  "video.skip_backward": "Salta enrere",
+  "video.skip_forward": "Salta endavant",
+  "video.unmute": "Deixa de silenciar",
+  "video.volume_down": "Abaixa el volum",
+  "video.volume_up": "Apuja el volum"
 }
diff --git a/app/javascript/mastodon/locales/ckb.json b/app/javascript/mastodon/locales/ckb.json
index 4ae3d38690..31f2dbbc11 100644
--- a/app/javascript/mastodon/locales/ckb.json
+++ b/app/javascript/mastodon/locales/ckb.json
@@ -28,7 +28,6 @@
   "account.endorse": "ناساندن لە پرۆفایل",
   "account.featured_tags.last_status_at": "دوایین پۆست لە {date}",
   "account.featured_tags.last_status_never": "هیچ پۆستێک نییە",
-  "account.featured_tags.title": "هاشتاگە تایبەتەکانی {name}",
   "account.follow": "بەدواداچوون",
   "account.follow_back": "فۆڵۆو بکەنەوە",
   "account.followers": "شوێنکەوتووان",
@@ -563,8 +562,6 @@
   "video.expand": "ڤیدیۆفراوان بکە",
   "video.fullscreen": "پڕپیشانگەر",
   "video.hide": "شاردنەوەی ڤیدیۆ",
-  "video.mute": "دەنگی کپ",
   "video.pause": "وەستان",
-  "video.play": "لێی بدە",
-  "video.unmute": "بێدەنگی مەکە"
+  "video.play": "لێی بدە"
 }
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index a452e81d88..c61b8484b4 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -341,8 +341,6 @@
   "video.expand": "Ingrandà a video",
   "video.fullscreen": "Pienu screnu",
   "video.hide": "Piattà a video",
-  "video.mute": "Surdina",
   "video.pause": "Pausa",
-  "video.play": "Lettura",
-  "video.unmute": "Caccià a surdina"
+  "video.play": "Lettura"
 }
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 66cc0727b6..0d8653d412 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Upravit profil",
   "account.enable_notifications": "Oznamovat mi příspěvky @{name}",
   "account.endorse": "Zvýraznit na profilu",
+  "account.featured": "Doporučené",
+  "account.featured.hashtags": "Hashtagy",
+  "account.featured.posts": "Příspěvky",
   "account.featured_tags.last_status_at": "Poslední příspěvek {date}",
   "account.featured_tags.last_status_never": "Žádné příspěvky",
-  "account.featured_tags.title": "Hlavní hashtagy uživatele {name}",
   "account.follow": "Sledovat",
   "account.follow_back": "Také sledovat",
   "account.followers": "Sledující",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
   "account.unblock": "Odblokovat @{name}",
   "account.unblock_domain": "Odblokovat doménu {domain}",
+  "account.unblock_domain_short": "Odblokovat",
   "account.unblock_short": "Odblokovat",
   "account.unendorse": "Nezvýrazňovat na profilu",
   "account.unfollow": "Přestat sledovat",
@@ -79,17 +82,17 @@
   "admin.dashboard.retention.cohort_size": "Noví uživatelé",
   "admin.impact_report.instance_accounts": "Profily účtů, které by byli odstaněny",
   "admin.impact_report.instance_followers": "Sledující, o které by naši uživatelé přišli",
-  "admin.impact_report.instance_follows": "Sledující, o které by naši uživatelé přišli",
+  "admin.impact_report.instance_follows": "Sledující, o které by jejich uživatelé přišli",
   "admin.impact_report.title": "Shrnutí dopadu",
   "alert.rate_limited.message": "Zkuste to prosím znovu po {retry_time, time, medium}.",
   "alert.rate_limited.title": "Spojení omezena",
   "alert.unexpected.message": "Objevila se neočekávaná chyba.",
   "alert.unexpected.title": "Jejda!",
   "alt_text_badge.title": "Popisek",
-  "alt_text_modal.add_alt_text": "Přidat alt text",
+  "alt_text_modal.add_alt_text": "Přidat popisek",
   "alt_text_modal.add_text_from_image": "Přidat text z obrázku",
   "alt_text_modal.cancel": "Zrušit",
-  "alt_text_modal.change_thumbnail": "Změnit miniaturu",
+  "alt_text_modal.change_thumbnail": "Změnit náhled",
   "alt_text_modal.describe_for_people_with_hearing_impairments": "Popište to pro osoby se sluchovým postižením…",
   "alt_text_modal.describe_for_people_with_visual_impairments": "Popište to pro osoby se zrakovým postižením…",
   "alt_text_modal.done": "Hotovo",
@@ -101,7 +104,7 @@
   "annual_report.summary.archetype.replier": "Sociální motýlek",
   "annual_report.summary.followers.followers": "sledujících",
   "annual_report.summary.followers.total": "{count} celkem",
-  "annual_report.summary.here_it_is": "Zde je tvůj {year} v přehledu:",
+  "annual_report.summary.here_it_is": "Zde je tvůj rok {year} v přehledu:",
   "annual_report.summary.highlighted_post.by_favourites": "nejvíce oblíbený příspěvek",
   "annual_report.summary.highlighted_post.by_reblogs": "nejvíce boostovaný příspěvek",
   "annual_report.summary.highlighted_post.by_replies": "příspěvek s nejvíce odpověďmi",
@@ -110,8 +113,8 @@
   "annual_report.summary.most_used_hashtag.most_used_hashtag": "nejpoužívanější hashtag",
   "annual_report.summary.most_used_hashtag.none": "Žádné",
   "annual_report.summary.new_posts.new_posts": "nové příspěvky",
-  "annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do vrcholu</topLabel><percentage></percentage><bottomLabel>{domain} uživatelů.</bottomLabel>",
-  "annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka zůstane mezi námi ;).",
+  "annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do horních</topLabel><percentage></percentage><bottomLabel> uživatelů domény {domain}.</bottomLabel>",
+  "annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka, zůstane mezi námi ;).",
   "annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!",
   "attachments_list.unprocessed": "(nezpracováno)",
   "audio.hide": "Skrýt zvuk",
@@ -218,6 +221,10 @@
   "confirmations.logout.confirm": "Odhlásit se",
   "confirmations.logout.message": "Opravdu se chcete odhlásit?",
   "confirmations.logout.title": "Odhlásit se?",
+  "confirmations.missing_alt_text.confirm": "Přidat popisek",
+  "confirmations.missing_alt_text.message": "Váš příspěvek obsahuje média bez popisku. Jejich přidání pomáhá zpřístupnit váš obsah většímu počtu lidí.",
+  "confirmations.missing_alt_text.secondary": "Přesto odeslat",
+  "confirmations.missing_alt_text.title": "Přidat popisek?",
   "confirmations.mute.confirm": "Skrýt",
   "confirmations.redraft.confirm": "Smazat a přepsat",
   "confirmations.redraft.message": "Jste si jistí, že chcete odstranit tento příspěvek a vytvořit z něj koncept? Oblíbené a boosty budou ztraceny a odpovědi na původní příspěvek ztratí kontext.",
@@ -257,18 +264,18 @@
   "domain_block_modal.they_wont_know": "Nebude vědět, že je zablokován*a.",
   "domain_block_modal.title": "Blokovat doménu?",
   "domain_block_modal.you_will_lose_num_followers": "Ztratíte {followersCount, plural, one {{followersCountDisplay} sledujícího} few {{followersCountDisplay} sledující} many {{followersCountDisplay} sledujících} other {{followersCountDisplay} sledujících}} a {followingCount, plural, one {{followingCountDisplay} sledovaného} few {{followingCountDisplay} sledované} many {{followingCountDisplay} sledovaných} other {{followingCountDisplay} sledovaných}}.",
-  "domain_block_modal.you_will_lose_relationships": "Ztratíte všechny sledující a lidi, které sledujete z tohoto serveru.",
+  "domain_block_modal.you_will_lose_relationships": "Ztratíte všechny sledující a sledované z tohoto serveru.",
   "domain_block_modal.you_wont_see_posts": "Neuvidíte příspěvky ani upozornění od uživatelů z tohoto serveru.",
-  "domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i s dalšími sociálními aplikacemi.",
+  "domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i na jiných sociálních aplikacích.",
   "domain_pill.activitypub_like_language": "ActivityPub je jako jazyk, kterým Mastodon mluví s jinými sociálními sítěmi.",
   "domain_pill.server": "Server",
   "domain_pill.their_handle": "Handle:",
-  "domain_pill.their_server": "Jejich digitální domov, kde žijí jejich všechny příspěvky.",
-  "domain_pill.their_username": "Jejich jedinečný identikátor na jejich serveru. Je možné najít uživatele se stejným uživatelským jménem na jiných serverech.",
+  "domain_pill.their_server": "Jejich digitální domov, kde žijí všechny jejich příspěvky.",
+  "domain_pill.their_username": "Jejich jedinečný identifikátor na jejich serveru. Je možné, že na jiných serverech jsou uživatelé se stejným uživatelským jménem.",
   "domain_pill.username": "Uživatelské jméno",
   "domain_pill.whats_in_a_handle": "Co obsahuje handle?",
-  "domain_pill.who_they_are": "Protože handle říkají kdo je kdo a také kde, je možné interagovat s lidmi napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
-  "domain_pill.who_you_are": "Protože handle říká kdo jsi a kde jsi, mohou s tebou lidé komunikovat napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
+  "domain_pill.who_they_are": "Protože handly říkají kdo je kdo a také kde, je možné interagovat s lidmi napříč sociálním webem, skládajícím se z <button>platforem postavených na ActivityPub</button>.",
+  "domain_pill.who_you_are": "Protože handle říká kdo jsi a kde jsi, mohou s tebou komunikovat lidé napříč sociálním webem, skládajícím se z <button>platforem postavených na ActivityPub</button>.",
   "domain_pill.your_handle": "Tvůj handle:",
   "domain_pill.your_server": "Tvůj digitální domov, kde žijí všechny tvé příspěvky. Nelíbí se ti? Kdykoliv se přesuň na jiný server a vezmi si sebou i své sledující.",
   "domain_pill.your_username": "Tvůj jedinečný identifikátor na tomto serveru. Je možné najít uživatele se stejným uživatelským jménem na jiných serverech.",
@@ -289,7 +296,8 @@
   "emoji_button.search_results": "Výsledky hledání",
   "emoji_button.symbols": "Symboly",
   "emoji_button.travel": "Cestování a místa",
-  "empty_column.account_hides_collections": "Tento uživatel se rozhodl nezveřejňovat tuto informaci",
+  "empty_column.account_featured": "Tento seznam je prázdný",
+  "empty_column.account_hides_collections": "Tento uživatel se rozhodl tuto informaci nezveřejňovat",
   "empty_column.account_suspended": "Účet je pozastaven",
   "empty_column.account_timeline": "Nejsou tu žádné příspěvky!",
   "empty_column.account_unavailable": "Profil není dostupný",
@@ -373,6 +381,8 @@
   "generic.saved": "Uloženo",
   "getting_started.heading": "Začínáme",
   "hashtag.admin_moderation": "Otevřít moderátorské rozhraní pro #{name}",
+  "hashtag.browse": "Procházet příspěvky na #{hashtag}",
+  "hashtag.browse_from_account": "Procházet příspěvky od @{name} v #{hashtag}",
   "hashtag.column_header.tag_mode.all": "a {additional}",
   "hashtag.column_header.tag_mode.any": "nebo {additional}",
   "hashtag.column_header.tag_mode.none": "bez {additional}",
@@ -386,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} other {{counter} příspěvků}}",
   "hashtag.counter_by_uses_today": "Dnes {count, plural, one {{counter} příspěvek} few {{counter} příspěvky} other {{counter} příspěvků}}",
   "hashtag.follow": "Sledovat hashtag",
+  "hashtag.mute": "Skrýt #{hashtag}",
   "hashtag.unfollow": "Přestat sledovat hashtag",
   "hashtags.and_other": "…a {count, plural, one {# další} few {# další} other {# dalších}}",
   "hints.profiles.followers_may_be_missing": "Sledující mohou pro tento profil chybět.",
@@ -509,9 +520,9 @@
   "loading_indicator.label": "Načítání…",
   "media_gallery.hide": "Skrýt",
   "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
-  "mute_modal.hide_from_notifications": "Skrýt z notifikací",
+  "mute_modal.hide_from_notifications": "Skrýt z oznámení",
   "mute_modal.hide_options": "Skrýt možnosti",
-  "mute_modal.indefinite": "Dokud je neodkryju",
+  "mute_modal.indefinite": "Dokud je neodeberu ze ztišených",
   "mute_modal.show_options": "Zobrazit možnosti",
   "mute_modal.they_can_mention_and_follow": "Mohou vás zmínit a sledovat, ale neuvidíte je.",
   "mute_modal.they_wont_know": "Nebudou vědět, že byli skryti.",
@@ -520,7 +531,7 @@
   "mute_modal.you_wont_see_posts": "Stále budou moci vidět vaše příspěvky, ale vy jejich neuvidíte.",
   "navigation_bar.about": "O aplikaci",
   "navigation_bar.administration": "Administrace",
-  "navigation_bar.advanced_interface": "Otevřít pokročilé webové rozhraní",
+  "navigation_bar.advanced_interface": "Otevřít v pokročilém webovém rozhraní",
   "navigation_bar.blocks": "Blokovaní uživatelé",
   "navigation_bar.bookmarks": "Záložky",
   "navigation_bar.community_timeline": "Místní časová osa",
@@ -549,13 +560,13 @@
   "notification.admin.report": "Uživatel {name} nahlásil {target}",
   "notification.admin.report_account": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target} za {category}",
   "notification.admin.report_account_other": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target}",
-  "notification.admin.report_statuses": "{name} nahlásil {target} za {category}",
-  "notification.admin.report_statuses_other": "{name} nahlásil {target}",
+  "notification.admin.report_statuses": "{name} nahlásili {target} za {category}",
+  "notification.admin.report_statuses_other": "{name} nahlásili {target}",
   "notification.admin.sign_up": "Uživatel {name} se zaregistroval",
   "notification.admin.sign_up.name_and_others": "{name} a {count, plural, one {# další} few {# další} many {# dalších} other {# dalších}} se zaregistrovali",
   "notification.annual_report.message": "Váš #Wrapstodon {year} na Vás čeká! Podívejte se, jak vypadal tento Váš rok na Mastodonu!",
   "notification.annual_report.view": "Zobrazit #Wrapstodon",
-  "notification.favourite": "Uživatel {name} si oblíbil váš příspěvek",
+  "notification.favourite": "{name} si oblíbil váš příspěvek",
   "notification.favourite.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Váš příspěvek",
   "notification.favourite_pm": "{name} si oblíbil vaši soukromou zmínku",
   "notification.favourite_pm.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Vaši soukromou zmínku",
@@ -570,11 +581,11 @@
   "notification.mention": "Zmínka",
   "notification.mentioned_you": "{name} vás zmínil",
   "notification.moderation-warning.learn_more": "Zjistit více",
-  "notification.moderation_warning": "Obdrželi jste moderační varování",
+  "notification.moderation_warning": "Obdrželi jste varování od moderátorů",
   "notification.moderation_warning.action_delete_statuses": "Některé z vašich příspěvků byly odstraněny.",
   "notification.moderation_warning.action_disable": "Váš účet je zablokován.",
   "notification.moderation_warning.action_mark_statuses_as_sensitive": "Některé z vašich příspěvků byly označeny jako citlivé.",
-  "notification.moderation_warning.action_none": "Váš účet obdržel moderační varování.",
+  "notification.moderation_warning.action_none": "Váš účet obdržel varování od moderátorů.",
   "notification.moderation_warning.action_sensitive": "Vaše příspěvky budou od nynějška označeny jako citlivé.",
   "notification.moderation_warning.action_silence": "Váš účet byl omezen.",
   "notification.moderation_warning.action_suspend": "Váš účet byl pozastaven.",
@@ -606,7 +617,7 @@
   "notification_requests.maximize": "Maximalizovat",
   "notification_requests.minimize_banner": "Minimalizovat banner filtrovaných oznámení",
   "notification_requests.notifications_from": "Oznámení od {name}",
-  "notification_requests.title": "Vyfiltrovaná oznámení",
+  "notification_requests.title": "Filtrovaná oznámení",
   "notification_requests.view": "Zobrazit oznámení",
   "notifications.clear": "Vyčistit oznámení",
   "notifications.clear_confirmation": "Opravdu chcete trvale smazat všechna vaše oznámení?",
@@ -665,7 +676,7 @@
   "notifications_permission_banner.title": "Nenechte si nic uniknout",
   "onboarding.follows.back": "Zpět",
   "onboarding.follows.done": "Hotovo",
-  "onboarding.follows.empty": "Bohužel, žádné výsledky nelze momentálně zobrazit. Můžete zkusit vyhledat nebo procházet stránku s průzkumem a najít lidi, kteří budou sledovat, nebo to zkuste znovu později.",
+  "onboarding.follows.empty": "Bohužel, žádné výsledky nelze momentálně zobrazit. Můžete zkusit najít uživatele ke sledování za pomocí vyhledávání nebo na stránce „Objevit“, nebo to zkuste znovu později.",
   "onboarding.follows.search": "Hledat",
   "onboarding.follows.title": "Sledujte lidi a začněte",
   "onboarding.profile.discoverable": "Udělat svůj profil vyhledatelným",
@@ -693,7 +704,7 @@
   "poll_button.remove_poll": "Odebrat anketu",
   "privacy.change": "Změnit soukromí příspěvku",
   "privacy.direct.long": "Všichni zmínění v příspěvku",
-  "privacy.direct.short": "Vybraní lidé",
+  "privacy.direct.short": "Soukromá zmínka",
   "privacy.private.long": "Jen vaši sledující",
   "privacy.private.short": "Sledující",
   "privacy.public.long": "Kdokoliv na Mastodonu i mimo něj",
@@ -769,7 +780,7 @@
   "report_notification.categories.spam": "Spam",
   "report_notification.categories.spam_sentence": "spam",
   "report_notification.categories.violation": "Porušení pravidla",
-  "report_notification.categories.violation_sentence": "porušení pravidla",
+  "report_notification.categories.violation_sentence": "porušení pravidel",
   "report_notification.open": "Otevřít hlášení",
   "search.no_recent_searches": "Žádná nedávná vyhledávání",
   "search.placeholder": "Hledat",
@@ -868,7 +879,9 @@
   "subscribed_languages.target": "Změnit odebírané jazyky na {target}",
   "tabs_bar.home": "Domů",
   "tabs_bar.notifications": "Oznámení",
+  "terms_of_service.effective_as_of": "Platné od {date}",
   "terms_of_service.title": "Podmínky služby",
+  "terms_of_service.upcoming_changes_on": "Nadcházející změny v {date}",
   "time_remaining.days": "{number, plural, one {Zbývá # den} few {Zbývají # dny} many {Zbývá # dní} other {Zbývá # dní}}",
   "time_remaining.hours": "{number, plural, one {Zbývá # hodina} few {Zbývají # hodiny} many {Zbývá # hodin} other {Zbývá # hodin}}",
   "time_remaining.minutes": "{number, plural, one {Zbývá # minuta} few {Zbývají # minuty} many {Zbývá # minut} other {Zbývá # minut}}",
@@ -899,8 +912,12 @@
   "video.expand": "Rozbalit video",
   "video.fullscreen": "Režim celé obrazovky",
   "video.hide": "Skrýt video",
-  "video.mute": "Vypnout zvuk",
+  "video.mute": "Ztlumit",
   "video.pause": "Pauza",
   "video.play": "Přehrát",
-  "video.unmute": "Zapnout zvuk"
+  "video.skip_backward": "Přeskočit zpět",
+  "video.skip_forward": "Přeskočit vpřed",
+  "video.unmute": "Zrušit ztlumení",
+  "video.volume_down": "Snížit hlasitost",
+  "video.volume_up": "Zvýšit hlasitost"
 }
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index b30ae5e215..3bf10be7fb 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -1,5 +1,5 @@
 {
-  "about.blocks": "Gweinyddion gyda chymedrolwyr",
+  "about.blocks": "Gweinyddion wedi'u cymedroli",
   "about.contact": "Cysylltwch â:",
   "about.disclaimer": "Mae Mastodon yn feddalwedd cod agored rhydd ac o dan hawlfraint Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Nid yw'r rheswm ar gael",
@@ -16,22 +16,24 @@
   "account.badges.bot": "Awtomataidd",
   "account.badges.group": "Grŵp",
   "account.block": "Blocio @{name}",
-  "account.block_domain": "Blocio parth {domain}",
+  "account.block_domain": "Blocio'r parth {domain}",
   "account.block_short": "Blocio",
   "account.blocked": "Blociwyd",
   "account.cancel_follow_request": "Tynnu cais i ddilyn",
   "account.copy": "Copïo dolen i'r proffil",
   "account.direct": "Crybwyll yn breifat @{name}",
   "account.disable_notifications": "Stopiwch fy hysbysu pan fydd @{name} yn postio",
-  "account.domain_blocked": "Parth wedi ei flocio",
+  "account.domain_blocked": "Parth wedi'i rwystro",
   "account.edit_profile": "Golygu proffil",
   "account.enable_notifications": "Rhowch wybod i fi pan fydd @{name} yn postio",
   "account.endorse": "Dangos ar fy mhroffil",
-  "account.featured_tags.last_status_at": "Y postiad diwethaf ar {date}",
+  "account.featured": "Dethol",
+  "account.featured.hashtags": "Hashnodau",
+  "account.featured.posts": "Postiadau",
+  "account.featured_tags.last_status_at": "Y postiad olaf ar {date}",
   "account.featured_tags.last_status_never": "Dim postiadau",
-  "account.featured_tags.title": "Prif hashnodau {name}",
   "account.follow": "Dilyn",
-  "account.follow_back": "Dilyn yn ôl",
+  "account.follow_back": "Dilyn nôl",
   "account.followers": "Dilynwyr",
   "account.followers.empty": "Does neb yn dilyn y defnyddiwr hwn eto.",
   "account.followers_counter": "{count, plural, one {{counter} dilynwr} two {{counter} ddilynwr} other {{counter} dilynwyr}}",
@@ -41,7 +43,7 @@
   "account.go_to_profile": "Mynd i'r proffil",
   "account.hide_reblogs": "Cuddio hybiau gan @{name}",
   "account.in_memoriam": "Er Cof.",
-  "account.joined_short": "Wedi Ymuno",
+  "account.joined_short": "Ymunodd",
   "account.languages": "Newid ieithoedd wedi tanysgrifio iddyn nhw",
   "account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}",
   "account.locked_info": "Mae'r statws preifatrwydd cyfrif hwn wedi'i osod i fod ar glo. Mae'r perchennog yn adolygu'r sawl sy'n gallu eu dilyn.",
@@ -56,16 +58,17 @@
   "account.no_bio": "Dim disgrifiad wedi'i gynnig.",
   "account.open_original_page": "Agor y dudalen wreiddiol",
   "account.posts": "Postiadau",
-  "account.posts_with_replies": "Postiadau ac atebion",
+  "account.posts_with_replies": "Postiadau ac ymatebion",
   "account.report": "Adrodd @{name}",
   "account.requested": "Aros am gymeradwyaeth. Cliciwch er mwyn canslo cais dilyn",
   "account.requested_follow": "Mae {name} wedi gwneud cais i'ch dilyn",
-  "account.share": "Rhannwch broffil @{name}",
+  "account.share": "Rhannu proffil @{name}",
   "account.show_reblogs": "Dangos hybiau gan @{name}",
   "account.statuses_counter": "{count, plural, one {{counter} postiad} two {{counter} bostiad} few {{counter} phostiad} many {{counter} postiad} other {{counter} postiad}}",
-  "account.unblock": "Dadflocio @{name}",
-  "account.unblock_domain": "Dadflocio parth {domain}",
-  "account.unblock_short": "Dadflocio",
+  "account.unblock": "Dadrwystro @{name}",
+  "account.unblock_domain": "Dadrwystro parth {domain}",
+  "account.unblock_domain_short": "Dadrwystro",
+  "account.unblock_short": "Dadrwystro",
   "account.unendorse": "Peidio a'i ddangos ar fy mhroffil",
   "account.unfollow": "Dad-ddilyn",
   "account.unmute": "Dad-dewi {name}",
@@ -85,48 +88,55 @@
   "alert.rate_limited.title": "Cyfradd gyfyngedig",
   "alert.unexpected.message": "Digwyddodd gwall annisgwyl.",
   "alert.unexpected.title": "Wps!",
-  "alt_text_badge.title": "Testun Amgen",
+  "alt_text_badge.title": "Testun amgen",
+  "alt_text_modal.add_alt_text": "Ychwanegu testun amgen",
+  "alt_text_modal.add_text_from_image": "Ychwanegu testun o'r ddelwedd",
+  "alt_text_modal.cancel": "Diddymu",
+  "alt_text_modal.change_thumbnail": "Newid llun bach",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Disgrifiwch hyn ar gyfer pobl â nam ar eu clyw…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Disgrifiwch hyn ar gyfer pobl â nam ar eu golwg…",
+  "alt_text_modal.done": "Gorffen",
   "announcement.announcement": "Cyhoeddiad",
   "annual_report.summary.archetype.booster": "Y hyrwyddwr",
-  "annual_report.summary.archetype.lurker": "Yr arsylwr",
+  "annual_report.summary.archetype.lurker": "Y crwydryn",
   "annual_report.summary.archetype.oracle": "Yr oracl",
   "annual_report.summary.archetype.pollster": "Yr arholwr",
   "annual_report.summary.archetype.replier": "Y sbardunwr",
   "annual_report.summary.followers.followers": "dilynwyr",
-  "annual_report.summary.followers.total": "{count} cyfanswm",
+  "annual_report.summary.followers.total": "Cyfanswm o{count}",
   "annual_report.summary.here_it_is": "Dyma eich {year} yn gryno:",
   "annual_report.summary.highlighted_post.by_favourites": "postiad wedi'i ffefrynu fwyaf",
   "annual_report.summary.highlighted_post.by_reblogs": "postiad wedi'i hybu fwyaf",
-  "annual_report.summary.highlighted_post.by_replies": "postiad gyda'r ymatebion mwyaf",
+  "annual_report.summary.highlighted_post.by_replies": "postiad gyda'r nifer fwyaf o atebion",
   "annual_report.summary.highlighted_post.possessive": "{name}",
   "annual_report.summary.most_used_app.most_used_app": "ap a ddefnyddiwyd fwyaf",
   "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashnod a ddefnyddiwyd fwyaf",
   "annual_report.summary.most_used_hashtag.none": "Dim",
   "annual_report.summary.new_posts.new_posts": "postiadau newydd",
-  "annual_report.summary.percentile.text": "<topLabel>Mae hynny'n eich rhoi chi ar y brig</topLabel><percentage></percentage><bottomLabel> o ddefnyddiwr {domain}.</bottomLabel>",
-  "annual_report.summary.percentile.we_wont_tell_bernie": "Ni fyddwn yn dweud wrth Bernie.",
+  "annual_report.summary.percentile.text": "<topLabel>Mae hynny'n eich rhoi chi ymysg y</topLabel><percentage></percentage><bottomLabel>uchaf o ddefnyddwyr {domain}.</bottomLabel>",
+  "annual_report.summary.percentile.we_wont_tell_bernie": "Fyddwn ni ddim yn dweud wrth Bernie.",
   "annual_report.summary.thanks": "Diolch am fod yn rhan o Mastodon!",
   "attachments_list.unprocessed": "(heb eu prosesu)",
   "audio.hide": "Cuddio sain",
   "block_modal.remote_users_caveat": "Byddwn yn gofyn i'r gweinydd {domain} barchu eich penderfyniad. Fodd bynnag, nid yw cydymffurfiad wedi'i warantu gan y gall rhai gweinyddwyr drin rhwystro mewn ffyrdd gwahanol. Mae'n bosibl y bydd postiadau cyhoeddus yn dal i fod yn weladwy i ddefnyddwyr nad ydynt wedi mewngofnodi.",
   "block_modal.show_less": "Dangos llai",
   "block_modal.show_more": "Dangos rhagor",
-  "block_modal.they_cant_mention": "Nid ydynt yn gallu eich crybwyll na'ch dilyn.",
-  "block_modal.they_cant_see_posts": "Nid ydynt yn gallu gweld eich postiadau ac ni fyddwch yn gweld eu rhai hwy.",
-  "block_modal.they_will_know": "Gallant weld eu bod wedi'u rhwystro.",
+  "block_modal.they_cant_mention": "Dydyn nhw ddim yn gallu eich crybwyll na'ch dilyn.",
+  "block_modal.they_cant_see_posts": "Dydyn nhw ddim yn gallu gweld eich postiadau a fyddwch chi ddim yn gweld eu rhai nhw.",
+  "block_modal.they_will_know": "Gallan nhw weld eu bod wedi'u rhwystro.",
   "block_modal.title": "Blocio defnyddiwr?",
   "block_modal.you_wont_see_mentions": "Fyddwch chi ddim yn gweld postiadau sy'n sôn amdanyn nhw.",
   "boost_modal.combo": "Mae modd pwyso {combo} er mwyn hepgor hyn tro nesa",
   "boost_modal.reblog": "Hybu postiad?",
   "boost_modal.undo_reblog": "Dad-hybu postiad?",
   "bundle_column_error.copy_stacktrace": "Copïo'r adroddiad gwall",
-  "bundle_column_error.error.body": "Nid oedd modd cynhyrchu'r dudalen honno. Gall fod oherwydd gwall yn ein cod neu fater cydnawsedd porwr.",
+  "bundle_column_error.error.body": "Does dim modd cynhyrchu'r dudalen honno. Gall fod oherwydd gwall yn ein cod neu fater cydnawsedd porwr.",
   "bundle_column_error.error.title": "O na!",
   "bundle_column_error.network.body": "Bu gwall wrth geisio llwytho'r dudalen hon. Gall hyn fod oherwydd anhawster dros-dro gyda'ch cysylltiad gwe neu'r gweinydd hwn.",
   "bundle_column_error.network.title": "Gwall rhwydwaith",
   "bundle_column_error.retry": "Ceisiwch eto",
   "bundle_column_error.return": "Mynd i'r ffrwd gartref",
-  "bundle_column_error.routing.body": "Nid oedd modd canfod y dudalen honno. Ydych chi'n siŵr fod yr URL yn y bar cyfeiriad yn gywir?",
+  "bundle_column_error.routing.body": "Doedd dim modd canfod y dudalen honno. Ydych chi'n siŵr fod yr URL yn y bar cyfeiriad yn gywir?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Cau",
   "bundle_modal_error.message": "Aeth rhywbeth o'i le wrth lwytho'r sgrin hon.",
@@ -135,15 +145,15 @@
   "closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.",
   "closed_registrations_modal.find_another_server": "Dod o hyd i weinydd arall",
   "closed_registrations_modal.preamble": "Mae Mastodon wedi'i ddatganoli, felly does dim gwahaniaeth ble rydych chi'n creu eich cyfrif, byddwch chi'n gallu dilyn a rhyngweithio ag unrhyw un ar y gweinydd hwn. Gallwch hyd yn oed ei gynnal un eich hun!",
-  "closed_registrations_modal.title": "Ymgofrestru ar Mastodon",
+  "closed_registrations_modal.title": "Cofrestru ar Mastodon",
   "column.about": "Ynghylch",
-  "column.blocks": "Defnyddwyr a flociwyd",
+  "column.blocks": "Defnyddwyr wedi'u rhwystro",
   "column.bookmarks": "Llyfrnodau",
   "column.community": "Ffrwd lleol",
   "column.create_list": "Creu rhestr",
   "column.direct": "Crybwylliadau preifat",
   "column.directory": "Pori proffiliau",
-  "column.domain_blocks": "Parthau wedi'u blocio",
+  "column.domain_blocks": "Parthau wedi'u rhwystro",
   "column.edit_list": "Golygu rhestr",
   "column.favourites": "Ffefrynnau",
   "column.firehose": "Ffrydiau byw",
@@ -156,7 +166,7 @@
   "column.pins": "Postiadau wedi eu pinio",
   "column.public": "Ffrwd y ffederasiwn",
   "column_back_button.label": "Nôl",
-  "column_header.hide_settings": "Cuddio dewisiadau",
+  "column_header.hide_settings": "Cuddio'r dewisiadau",
   "column_header.moveLeft_settings": "Symud y golofn i'r chwith",
   "column_header.moveRight_settings": "Symud y golofn i'r dde",
   "column_header.pin": "Pinio",
@@ -174,7 +184,7 @@
   "compose.saved.body": "Postiad wedi'i gadw.",
   "compose_form.direct_message_warning_learn_more": "Dysgu mwy",
   "compose_form.encryption_warning": "Dyw postiadau ar Mastodon ddim wedi'u hamgryptio o ben i ben. Peidiwch â rhannu unrhyw wybodaeth sensitif dros Mastodon.",
-  "compose_form.hashtag_warning": "Ni fydd y postiad hwn wedi ei restru o dan unrhyw hashnod gan nad yw'n gyhoeddus. Dim ond postiadau cyhoeddus y mae modd eu chwilio drwy hashnod.",
+  "compose_form.hashtag_warning": "Fydd y postiad hwn ddim wedi'i restru o dan unrhyw hashnod gan nad yw'n gyhoeddus. Dim ond postiadau cyhoeddus y mae modd eu chwilio drwy hashnod.",
   "compose_form.lock_disclaimer": "Nid yw eich cyfri wedi'i {locked}. Gall unrhyw un eich dilyn i weld eich postiadau dilynwyr-yn-unig.",
   "compose_form.lock_disclaimer.lock": "wedi ei gloi",
   "compose_form.placeholder": "Beth sydd ar eich meddwl?",
@@ -185,15 +195,15 @@
   "compose_form.poll.switch_to_multiple": "Newid pleidlais i adael mwy nag un dewis",
   "compose_form.poll.switch_to_single": "Newid pleidlais i gyfyngu i un dewis",
   "compose_form.poll.type": "Arddull",
-  "compose_form.publish": "Postiad",
+  "compose_form.publish": "Postio",
   "compose_form.publish_form": "Postiad newydd",
-  "compose_form.reply": "Ateb",
+  "compose_form.reply": "Ymateb",
   "compose_form.save_changes": "Diweddaru",
   "compose_form.spoiler.marked": "Dileu rhybudd cynnwys",
   "compose_form.spoiler.unmarked": "Ychwanegu rhybudd cynnwys",
   "compose_form.spoiler_placeholder": "Rhybudd cynnwys (dewisol)",
   "confirmation_modal.cancel": "Canslo",
-  "confirmations.block.confirm": "Blocio",
+  "confirmations.block.confirm": "Rhwystro",
   "confirmations.delete.confirm": "Dileu",
   "confirmations.delete.message": "Ydych chi'n sicr eich bod eisiau dileu y post hwn?",
   "confirmations.delete.title": "Dileu postiad?",
@@ -211,11 +221,15 @@
   "confirmations.logout.confirm": "Allgofnodi",
   "confirmations.logout.message": "Ydych chi'n siŵr eich bod am allgofnodi?",
   "confirmations.logout.title": "Allgofnodi?",
+  "confirmations.missing_alt_text.confirm": "Ychwanegu testun amgen",
+  "confirmations.missing_alt_text.message": "Mae eich postiad yn cynnwys cyfryngau heb destun amgen. Mae ychwanegu disgrifiadau yn helpu i wneud eich cynnwys yn hygyrch i fwy o bobl.",
+  "confirmations.missing_alt_text.secondary": "Postio beth bynnag",
+  "confirmations.missing_alt_text.title": "Ychwanegu testun amgen?",
   "confirmations.mute.confirm": "Tewi",
   "confirmations.redraft.confirm": "Dileu ac ailddrafftio",
   "confirmations.redraft.message": "Ydych chi wir eisiau'r dileu'r postiad hwn a'i ailddrafftio? Bydd ffefrynnau a hybiau'n cael eu colli, a bydd atebion i'r post gwreiddiol yn mynd yn amddifad.",
   "confirmations.redraft.title": "Dileu & ailddraftio postiad?",
-  "confirmations.reply.confirm": "Ateb",
+  "confirmations.reply.confirm": "Ymateb",
   "confirmations.reply.message": "Bydd ateb nawr yn cymryd lle y neges yr ydych yn cyfansoddi ar hyn o bryd. Ydych chi'n siŵr eich bod am barhau?",
   "confirmations.reply.title": "Trosysgrifo'r postiad?",
   "confirmations.unfollow.confirm": "Dad-ddilyn",
@@ -237,16 +251,16 @@
   "directory.recently_active": "Ar-lein yn ddiweddar",
   "disabled_account_banner.account_settings": "Gosodiadau'r cyfrif",
   "disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.",
-  "dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl sydd â chyfrifon ar {domain}.",
-  "dismissable_banner.dismiss": "Cau",
-  "dismissable_banner.explore_links": "Y straeon newyddion hyn yw'r rhai sy'n cael eu rhannu fwyaf ar y ffederasiwn heddiw. Mae straeon newyddion mwy diweddar sy'n cael eu postio gan fwy o amrywiaeth o bobl yn cael eu graddio'n uwch.",
+  "dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl y caiff eu cyfrifon eu cynnal ar {domain}.",
+  "dismissable_banner.dismiss": "Diystyru",
+  "dismissable_banner.explore_links": "Y straeon newyddion hyn yw'r rhai sy'n cael eu rhannu fwyaf ar y ffedysawd heddiw. Mae straeon newyddion mwy diweddar sy'n cael eu postio gan fwy o amrywiaeth o bobl yn cael eu graddio'n uwch.",
   "dismissable_banner.explore_statuses": "Mae'r postiadau hyn o bob rhan o'r ffedysawd yn cael mwy o sylw heddiw. Mae postiadau mwy diweddar sydd â mwy o hybu a ffefrynnu'n cael eu graddio'n uwch.",
   "dismissable_banner.explore_tags": "Mae'r hashnodau hyn ar gynnydd y ffedysawd heddiw. Mae hashnodau sy'n cael eu defnyddio gan fwy o bobl amrywiol yn cael eu graddio'n uwch.",
   "dismissable_banner.public_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl ar y ffedysawd y mae pobl ar {domain} yn eu dilyn.",
   "domain_block_modal.block": "Blocio gweinydd",
   "domain_block_modal.block_account_instead": "Blocio @{name} yn ei le",
   "domain_block_modal.they_can_interact_with_old_posts": "Gall pobl o'r gweinydd hwn ryngweithio â'ch hen bostiadau.",
-  "domain_block_modal.they_cant_follow": "Ni all neb o'r gweinydd hwn eich dilyn.",
+  "domain_block_modal.they_cant_follow": "All neb o'r gweinydd hwn eich dilyn.",
   "domain_block_modal.they_wont_know": "Fyddan nhw ddim yn gwybod eu bod wedi cael eu blocio.",
   "domain_block_modal.title": "Blocio parth?",
   "domain_block_modal.you_will_lose_num_followers": "Byddwch yn colli {followersCount, plural, one {{followersCountDisplay} dilynwr} other {{followersCountDisplay} dilynwyr}} a {followingCount, plural, one {{followingCountDisplay} person rydych yn dilyn} other {{followingCountDisplay} o bobl rydych yn eu dilyn}}.",
@@ -282,14 +296,15 @@
   "emoji_button.search_results": "Canlyniadau chwilio",
   "emoji_button.symbols": "Symbolau",
   "emoji_button.travel": "Teithio a Llefydd",
+  "empty_column.account_featured": "Mae'r rhestr hon yn wag",
   "empty_column.account_hides_collections": "Mae'r defnyddiwr wedi dewis i beidio rhannu'r wybodaeth yma",
   "empty_column.account_suspended": "Cyfrif wedi'i atal",
   "empty_column.account_timeline": "Dim postiadau yma!",
   "empty_column.account_unavailable": "Nid yw'r proffil ar gael",
-  "empty_column.blocks": "Nid ydych wedi blocio unrhyw ddefnyddwyr eto.",
-  "empty_column.bookmarked_statuses": "Nid oes gennych unrhyw bostiad wedi'u cadw fel nod tudalen eto. Pan fyddwch yn gosod nod tudalen i un, mi fydd yn ymddangos yma.",
+  "empty_column.blocks": "Dydych chi heb rwystro unrhyw ddefnyddwyr eto.",
+  "empty_column.bookmarked_statuses": "Does gennych chi ddim unrhyw bostiad wedi'u cadw fel nod tudalen eto. Pan fyddwch yn gosod nod tudalen i un, mi fydd yn ymddangos yma.",
   "empty_column.community": "Mae'r ffrwd lleol yn wag. Beth am ysgrifennu rhywbeth cyhoeddus!",
-  "empty_column.direct": "Nid oes gennych unrhyw grybwylliadau preifat eto. Pan fyddwch chi'n anfon neu'n derbyn un, bydd yn ymddangos yma.",
+  "empty_column.direct": "Does gennych chi unrhyw grybwylliadau preifat eto. Pan fyddwch chi'n anfon neu'n derbyn un, bydd yn ymddangos yma.",
   "empty_column.domain_blocks": "Nid oes unrhyw barthau wedi'u blocio eto.",
   "empty_column.explore_statuses": "Does dim pynciau llosg ar hyn o bryd. Dewch nôl nes ymlaen!",
   "empty_column.favourited_statuses": "Rydych chi heb ffafrio unrhyw bostiadau eto. Pan byddwch chi'n ffafrio un, bydd yn ymddangos yma.",
@@ -387,10 +402,10 @@
   "hints.profiles.see_more_followers": "Gweld mwy o ddilynwyr ar {domain}",
   "hints.profiles.see_more_follows": "Gweld mwy o 'yn dilyn' ar {domain}",
   "hints.profiles.see_more_posts": "Gweld mwy o bostiadau ar {domain}",
-  "hints.threads.replies_may_be_missing": "Mae'n bosibl y bydd atebion gan weinyddion eraill ar goll.",
-  "hints.threads.see_more": "Gweld mwy o atebion ar {domain}",
+  "hints.threads.replies_may_be_missing": "Mae'n bosibl y bydd ymatebion gan weinyddion eraill ar goll.",
+  "hints.threads.see_more": "Gweld mwy o ymatebion ar {domain}",
   "home.column_settings.show_reblogs": "Dangos hybiau",
-  "home.column_settings.show_replies": "Dangos atebion",
+  "home.column_settings.show_replies": "Dangos ymatebion",
   "home.hide_announcements": "Cuddio cyhoeddiadau",
   "home.pending_critical_update.body": "Diweddarwch eich gweinydd Mastodon cyn gynted â phosibl!",
   "home.pending_critical_update.link": "Gweld diweddariadau",
@@ -407,10 +422,12 @@
   "ignore_notifications_modal.not_followers_title": "Anwybyddu hysbysiadau gan bobl nad ydynt yn eich dilyn?",
   "ignore_notifications_modal.not_following_title": "Anwybyddu hysbysiadau gan bobl nad ydych yn eu dilyn?",
   "ignore_notifications_modal.private_mentions_title": "Anwybyddu hysbysiadau o Grybwylliadau Preifat digymell?",
+  "info_button.label": "Cymorth",
+  "info_button.what_is_alt_text": "<h1>Beth yw testun amgen?</h1><p> Mae Testun Amgen yn darparu disgrifiadau delwedd ar gyfer pobl â nam ar eu golwg, cysylltiadau lled band isel, neu'r rhai sy'n ceisio cyd-destun ychwanegol.</p><p> Gallwch wella hygyrchedd a dealltwriaeth i bawb trwy ysgrifennu testun amgen clir, cryno a gwrthrychol.</p><ul><li> Dal elfennau pwysig</li><li> Crynhoi testun mewn delweddau</li><li> Defnyddiwch strwythur brawddegau rheolaidd</li><li> Osgoi gwybodaeth ddiangen</li><li> Canolbwyntio ar dueddiadau a chanfyddiadau allweddol mewn delweddau cymhleth (fel diagramau neu fapiau)</li></ul>",
   "interaction_modal.action.favourite": "I barhau, mae angen i chi hoffi o'ch cyfrif.",
   "interaction_modal.action.follow": "I barhau, mae angen i chi ddilyn o'ch cyfrif.",
   "interaction_modal.action.reblog": "I barhau, mae angen i chi ail-flogio o'ch cyfrif.",
-  "interaction_modal.action.reply": "I barhau, mae angen i chi ateb o'ch cyfrif.",
+  "interaction_modal.action.reply": "I barhau, mae angen i chi ymateb o'ch cyfrif.",
   "interaction_modal.action.vote": "I barhau, mae angen i chi bleidleisio o'ch cyfrif.",
   "interaction_modal.go": "Mynd",
   "interaction_modal.no_account_yet": "Dim cyfrif eto?",
@@ -449,7 +466,7 @@
   "keyboard_shortcuts.open_media": "Agor cyfryngau",
   "keyboard_shortcuts.pinned": "Agor rhestr postiadau wedi'u pinio",
   "keyboard_shortcuts.profile": "Agor proffil yr awdur",
-  "keyboard_shortcuts.reply": "Ateb i bostiad",
+  "keyboard_shortcuts.reply": "Ymateb i bostiad",
   "keyboard_shortcuts.requests": "Agor rhestr ceisiadau dilyn",
   "keyboard_shortcuts.search": "Ffocysu ar y bar chwilio",
   "keyboard_shortcuts.spoilers": "Dangos/cuddio'r maes CW",
@@ -476,9 +493,9 @@
   "lists.create": "Creu",
   "lists.create_a_list_to_organize": "Creu rhestr newydd i drefnu eich llif Cartref",
   "lists.create_list": "Creu rhestr",
-  "lists.delete": "Dileu rhestr",
+  "lists.delete": "Dileu'r rhestr",
   "lists.done": "Wedi gorffen",
-  "lists.edit": "Golygu rhestr",
+  "lists.edit": "Golygu'r rhestr",
   "lists.exclusive": "Cuddio aelodau yn y Cartref",
   "lists.exclusive_hint": "Os oes rhywun ar y rhestr hon, cuddiwch nhw yn eich llif Cartref i osgoi gweld eu postiadau ddwywaith.",
   "lists.find_users_to_add": "Canfod defnyddwyr i'w hychwanegu",
@@ -495,11 +512,11 @@
   "lists.replies_policy.none": "Neb",
   "lists.save": "Cadw",
   "lists.search": "Chwilio",
-  "lists.show_replies_to": "Cynhwyswch atebion gan aelodau'r rhestr i",
+  "lists.show_replies_to": "Cynnwys ymatebion gan aelodau'r rhestr i",
   "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
   "loading_indicator.label": "Yn llwytho…",
   "media_gallery.hide": "Cuddio",
-  "moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
+  "moved_to_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd am i chi symud i {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau",
   "mute_modal.hide_options": "Cuddio'r dewis",
   "mute_modal.indefinite": "Nes i mi eu dad-dewi",
@@ -537,7 +554,7 @@
   "navigation_bar.search": "Chwilio",
   "navigation_bar.security": "Diogelwch",
   "not_signed_in_indicator.not_signed_in": "Rhaid i chi fewngofnodi i weld yr adnodd hwn.",
-  "notification.admin.report": "Adroddwyd ar {name} {target}",
+  "notification.admin.report": "Adroddodd {name} {target}",
   "notification.admin.report_account": "Adroddodd {name} {count, plural, one {un postiad} other {# postiad}} gan {target} oherwydd {category}",
   "notification.admin.report_account_other": "Adroddodd {name} {count, plural, one {un postiad} two {# bostiad} few {# postiad} other {# postiad}} gan {target}",
   "notification.admin.report_statuses": "Adroddodd {name} {target} ar gyfer {category}",
@@ -549,14 +566,15 @@
   "notification.favourite": "Ffafriodd {name} eich postiad",
   "notification.favourite.name_and_others_with_link": "Ffafriodd {name} a <a>{count, plural, one {# arall} other {# arall}}</a> eich postiad",
   "notification.favourite_pm": "Mae {name} wedi ffefrynnu eich cyfeiriad preifat",
+  "notification.favourite_pm.name_and_others_with_link": "Mae {name} a <a>{count, plural, one {# arall} other {# arall}}</a> wedi hoffi'ch crybwylliad preifat",
   "notification.follow": "Dilynodd {name} chi",
   "notification.follow.name_and_others": "Mae {name} a <a>{count, plural, zero {}one {# arall} two {# arall} few {# arall} many {# others} other {# arall}}</a> nawr yn eich dilyn chi",
   "notification.follow_request": "Mae {name} wedi gwneud cais i'ch dilyn",
   "notification.follow_request.name_and_others": "Mae {name} a{count, plural, one {# arall} other {# arall}} wedi gofyn i'ch dilyn chi",
   "notification.label.mention": "Crybwyll",
   "notification.label.private_mention": "Crybwyll preifat",
-  "notification.label.private_reply": "Ateb preifat",
-  "notification.label.reply": "Ateb",
+  "notification.label.private_reply": "Ymateb preifat",
+  "notification.label.reply": "Ymateb",
   "notification.mention": "Crybwyll",
   "notification.mentioned_you": "Rydych wedi'ch crybwyll gan {name}",
   "notification.moderation-warning.learn_more": "Dysgu mwy",
@@ -683,7 +701,7 @@
   "poll_button.remove_poll": "Tynnu pleidlais",
   "privacy.change": "Addasu preifatrwdd y post",
   "privacy.direct.long": "Pawb sydd â sôn amdanyn nhw yn y postiad",
-  "privacy.direct.short": "Pobl benodol",
+  "privacy.direct.short": "Crybwylliad preifat",
   "privacy.private.long": "Eich dilynwyr yn unig",
   "privacy.private.short": "Dilynwyr",
   "privacy.public.long": "Unrhyw ar ac oddi ar Mastodon",
@@ -717,7 +735,7 @@
   "report.categories.other": "Arall",
   "report.categories.spam": "Sbam",
   "report.categories.violation": "Mae cynnwys yn torri un neu fwy o reolau'r gweinydd",
-  "report.category.subtitle": "Dewiswch y gyfatebiaeth gorau",
+  "report.category.subtitle": "Dewiswch yr ateb gorau",
   "report.category.title": "Beth sy'n digwydd gyda'r {type} yma?",
   "report.category.title_account": "proffil",
   "report.category.title_status": "post",
@@ -839,7 +857,7 @@
   "status.remove_favourite": "Tynnu o'r ffefrynnau",
   "status.replied_in_thread": "Atebodd mewn edefyn",
   "status.replied_to": "Wedi ateb {name}",
-  "status.reply": "Ateb",
+  "status.reply": "Ymateb",
   "status.replyAll": "Ateb i edefyn",
   "status.report": "Adrodd ar @{name}",
   "status.sensitive_warning": "Cynnwys sensitif",
@@ -858,7 +876,9 @@
   "subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}",
   "tabs_bar.home": "Cartref",
   "tabs_bar.notifications": "Hysbysiadau",
+  "terms_of_service.effective_as_of": "Yn effeithiol ers {date}",
   "terms_of_service.title": "Telerau Gwasanaeth",
+  "terms_of_service.upcoming_changes_on": "Newidiadau i ddod ar {date}",
   "time_remaining.days": "{number, plural, one {# diwrnod} other {# diwrnod}} ar ôl",
   "time_remaining.hours": "{number, plural, one {# awr} other {# awr}} ar ôl",
   "time_remaining.minutes": "{number, plural, one {# munud} other {# munud}} ar ôl",
@@ -889,8 +909,12 @@
   "video.expand": "Ymestyn fideo",
   "video.fullscreen": "Sgrin llawn",
   "video.hide": "Cuddio fideo",
-  "video.mute": "Diffodd sain",
+  "video.mute": "Tewi",
   "video.pause": "Oedi",
   "video.play": "Chwarae",
-  "video.unmute": "Dad-dewi sain"
+  "video.skip_backward": "Symud nôl",
+  "video.skip_forward": "Symud ymlaen",
+  "video.unmute": "Dad-dewi",
+  "video.volume_down": "Lefel sain i lawr",
+  "video.volume_up": "Lefel sain i fyny"
 }
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 7f198727a9..e1d8e7aec2 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Redigér profil",
   "account.enable_notifications": "Advisér mig, når @{name} poster",
   "account.endorse": "Fremhæv på profil",
+  "account.featured": "Fremhævet",
+  "account.featured.hashtags": "Hashtags",
+  "account.featured.posts": "Indlæg",
   "account.featured_tags.last_status_at": "Seneste indlæg {date}",
   "account.featured_tags.last_status_never": "Ingen indlæg",
-  "account.featured_tags.title": "{name}s fremhævede hashtags",
   "account.follow": "Følg",
   "account.follow_back": "Følg tilbage",
   "account.followers": "Følgere",
@@ -39,7 +41,7 @@
   "account.following_counter": "{count, plural, one {{counter} følger} other {{counter} følger}}",
   "account.follows.empty": "Denne bruger følger ikke nogen endnu.",
   "account.go_to_profile": "Gå til profil",
-  "account.hide_reblogs": "Skjul boosts fra @{name}",
+  "account.hide_reblogs": "Skjul fremhævelser fra @{name}",
   "account.in_memoriam": "Til minde om.",
   "account.joined_short": "Oprettet",
   "account.languages": "Skift abonnementssprog",
@@ -49,9 +51,9 @@
   "account.mention": "Nævn @{name}",
   "account.moved_to": "{name} har angivet, at vedkommendes nye konto nu er:",
   "account.mute": "Skjul @{name}",
-  "account.mute_notifications_short": "Slå lyden fra for notifikationer",
-  "account.mute_short": "Skjul (mute)",
-  "account.muted": "Skjult (muted)",
+  "account.mute_notifications_short": "Sluk for notifikationer",
+  "account.mute_short": "Skjul",
+  "account.muted": "Skjult",
   "account.mutual": "Fælles",
   "account.no_bio": "Ingen beskrivelse til rådighed.",
   "account.open_original_page": "Åbn oprindelig side",
@@ -63,14 +65,15 @@
   "account.share": "Del @{name}s profil",
   "account.show_reblogs": "Vis fremhævelser fra @{name}",
   "account.statuses_counter": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
-  "account.unblock": "Afblokér @{name}",
-  "account.unblock_domain": "Afblokér domænet {domain}",
-  "account.unblock_short": "Afblokér",
+  "account.unblock": "Fjern blokering af @{name}",
+  "account.unblock_domain": "Fjern blokering af domænet {domain}",
+  "account.unblock_domain_short": "Afblokér",
+  "account.unblock_short": "Fjern blokering",
   "account.unendorse": "Fjern visning på din profil",
   "account.unfollow": "Følg ikke længere",
-  "account.unmute": "Vis @{name} igen (unmute)",
-  "account.unmute_notifications_short": "Slå lyden fra for notifikationer",
-  "account.unmute_short": "Vis igen (unmute)",
+  "account.unmute": "Vis @{name} igen",
+  "account.unmute_notifications_short": "Tænd for notifikationer",
+  "account.unmute_short": "Vis igen",
   "account_note.placeholder": "Klik for at tilføje notat",
   "admin.dashboard.daily_retention": "Brugerfastholdelsesrate per dag efter tilmelding",
   "admin.dashboard.monthly_retention": "Brugerfastholdelsesrate per måned efter tilmelding",
@@ -94,38 +97,38 @@
   "alt_text_modal.describe_for_people_with_visual_impairments": "Beskriv dette for personer med nedsat syn…",
   "alt_text_modal.done": "Færdig",
   "announcement.announcement": "Bekendtgørelse",
-  "annual_report.summary.archetype.booster": "Cool-hunter",
-  "annual_report.summary.archetype.lurker": "Lurker",
-  "annual_report.summary.archetype.oracle": "Oracle",
-  "annual_report.summary.archetype.pollster": "Pollster",
-  "annual_report.summary.archetype.replier": "Social butterfly",
+  "annual_report.summary.archetype.booster": "Fremhæveren",
+  "annual_report.summary.archetype.lurker": "Lureren",
+  "annual_report.summary.archetype.oracle": "Oraklet",
+  "annual_report.summary.archetype.pollster": "Afstemningsmageren",
+  "annual_report.summary.archetype.replier": "Den social sommerfugl",
   "annual_report.summary.followers.followers": "følgere",
   "annual_report.summary.followers.total": "{count} i alt",
-  "annual_report.summary.here_it_is": "Her er {year} i sammendrag:",
+  "annual_report.summary.here_it_is": "Her er dit {year} i sammendrag:",
   "annual_report.summary.highlighted_post.by_favourites": "mest favoritmarkerede indlæg",
-  "annual_report.summary.highlighted_post.by_reblogs": "mest boostede indlæg",
-  "annual_report.summary.highlighted_post.by_replies": "indlæg med flest svar",
+  "annual_report.summary.highlighted_post.by_reblogs": "mest fremhævede indlæg",
+  "annual_report.summary.highlighted_post.by_replies": "mest besvarede indlæg",
   "annual_report.summary.highlighted_post.possessive": "{name}s",
   "annual_report.summary.most_used_app.most_used_app": "mest benyttede app",
-  "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest benyttede hashtag",
+  "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest benyttede etiket",
   "annual_report.summary.most_used_hashtag.none": "Intet",
   "annual_report.summary.new_posts.new_posts": "nye indlæg",
   "annual_report.summary.percentile.text": "<topLabel>Det betyder, at man er i top</topLabel><percentage></percentage><bottomLabel>af {domain}-brugere.</bottomLabel>",
-  "annual_report.summary.percentile.we_wont_tell_bernie": "Vi fortæller det ikke til Bernie.",
+  "annual_report.summary.percentile.we_wont_tell_bernie": "Vi fortæller det ikke til Pernille Skipper.",
   "annual_report.summary.thanks": "Tak for at være en del af Mastodon!",
   "attachments_list.unprocessed": "(ubehandlet)",
   "audio.hide": "Skjul lyd",
   "block_modal.remote_users_caveat": "Serveren {domain} vil blive bedt om at respektere din beslutning. Overholdelse er dog ikke garanteret, da nogle servere kan håndtere blokke forskelligt. Offentlige indlæg kan stadig være synlige for ikke-indloggede brugere.",
-  "block_modal.show_less": "Vis mindre",
+  "block_modal.show_less": "Vis færre",
   "block_modal.show_more": "Vis flere",
-  "block_modal.they_cant_mention": "Vedkommende kan ikke nævne eller følge dig.",
+  "block_modal.they_cant_mention": "Vedkommende kan ikke omtale eller følge dig.",
   "block_modal.they_cant_see_posts": "Vedkommende kan ikke se dine indlæg, og du vil ikke se vedkommendes.",
   "block_modal.they_will_know": "Vedkommende kan se den aktive blokering.",
   "block_modal.title": "Blokér bruger?",
-  "block_modal.you_wont_see_mentions": "Du vil ikke se indlæg, som nævner vedkommende.",
+  "block_modal.you_wont_see_mentions": "Du vil ikke se indlæg, som omtaler vedkommende.",
   "boost_modal.combo": "Du kan trykke {combo} for at springe dette over næste gang",
-  "boost_modal.reblog": "Boost indlæg?",
-  "boost_modal.undo_reblog": "Fjern boost af indlæg?",
+  "boost_modal.reblog": "Fremhæv indlæg?",
+  "boost_modal.undo_reblog": "Fjern fremhævning af indlæg?",
   "bundle_column_error.copy_stacktrace": "Kopiér fejlrapport",
   "bundle_column_error.error.body": "Den anmodede side kunne ikke gengives. Dette kan skyldes flere typer fejl.",
   "bundle_column_error.error.title": "Åh nej!",
@@ -153,12 +156,12 @@
   "column.domain_blocks": "Blokerede domæner",
   "column.edit_list": "Redigér liste",
   "column.favourites": "Favoritter",
-  "column.firehose": "Live feeds",
+  "column.firehose": "Aktuelt",
   "column.follow_requests": "Følgeanmodninger",
   "column.home": "Hjem",
   "column.list_members": "Håndtér listemedlemmer",
   "column.lists": "Lister",
-  "column.mutes": "Skjulte brugere (mutede)",
+  "column.mutes": "Skjulte brugere",
   "column.notifications": "Notifikationer",
   "column.pins": "Fastgjorte indlæg",
   "column.public": "Fælles tidslinje",
@@ -168,7 +171,7 @@
   "column_header.moveRight_settings": "Flyt kolonne til højre",
   "column_header.pin": "Fastgør",
   "column_header.show_settings": "Vis indstillinger",
-  "column_header.unpin": "Løsgør",
+  "column_header.unpin": "Frigør",
   "column_search.cancel": "Afbryd",
   "column_subheading.settings": "Indstillinger",
   "community.column_settings.local_only": "Kun lokalt",
@@ -181,7 +184,7 @@
   "compose.saved.body": "Indlæg gemt.",
   "compose_form.direct_message_warning_learn_more": "Få mere at vide",
   "compose_form.encryption_warning": "Indlæg på Mastodon er ikke ende-til-ende-krypteret. Del derfor ikke sensitiv information via Mastodon.",
-  "compose_form.hashtag_warning": "Da indlægget ikke er offentligt, vises det ikke under noget hashtag, da kun offentlige indlæg er søgbare via hashtags.",
+  "compose_form.hashtag_warning": "Da indlægget ikke er offentligt, vises det ikke under nogen etiket, da kun offentlige indlæg er søgbare via etiketter.",
   "compose_form.lock_disclaimer": "Din konto er ikke {locked}. Enhver kan følge dig og se indlæg kun beregnet for følgere.",
   "compose_form.lock_disclaimer.lock": "låst",
   "compose_form.placeholder": "Hvad tænker du på?",
@@ -196,9 +199,9 @@
   "compose_form.publish_form": "Publicér",
   "compose_form.reply": "Svar",
   "compose_form.save_changes": "Opdatér",
-  "compose_form.spoiler.marked": "Fjern indholdsadvarsel",
-  "compose_form.spoiler.unmarked": "Tilføj indholdsadvarsel",
-  "compose_form.spoiler_placeholder": "Indholdsadvarsel (valgfri)",
+  "compose_form.spoiler.marked": "Fjern emnefelt",
+  "compose_form.spoiler.unmarked": "Tilføj emnefelt",
+  "compose_form.spoiler_placeholder": "Emnefelt (valgfrit)",
   "confirmation_modal.cancel": "Afbryd",
   "confirmations.block.confirm": "Blokér",
   "confirmations.delete.confirm": "Slet",
@@ -222,9 +225,9 @@
   "confirmations.missing_alt_text.message": "Indlægget indeholder medier uden alt-tekst. Tilføjelse af beskrivelser bidrager til at gøre indholdet tilgængeligt for flere brugere.",
   "confirmations.missing_alt_text.secondary": "Læg op alligevel",
   "confirmations.missing_alt_text.title": "Tilføj alt-tekst?",
-  "confirmations.mute.confirm": "Skjul (mute)",
+  "confirmations.mute.confirm": "Skjul",
   "confirmations.redraft.confirm": "Slet og omformulér",
-  "confirmations.redraft.message": "Sikker på, at dette indlæg skal slettes og omskrives? Favoritter og boosts går tabt, og svar til det oprindelige indlæg mister tilknytningen.",
+  "confirmations.redraft.message": "Sikker på, at dette indlæg skal slettes og omskrives? Favoritter og fremhævelser går tabt, og svar til det oprindelige indlæg mister tilknytningen.",
   "confirmations.redraft.title": "Slet og omformulér indlæg?",
   "confirmations.reply.confirm": "Svar",
   "confirmations.reply.message": "Hvis du svarer nu, vil det overskrive den besked, du er ved at skrive. Fortsæt alligevel?",
@@ -242,7 +245,7 @@
   "copy_icon_button.copied": "Kopieret til udklipsholderen",
   "copypaste.copied": "Kopieret",
   "copypaste.copy_to_clipboard": "Kopiér til udklipsholder",
-  "directory.federated": "Fra kendt fedivers",
+  "directory.federated": "Fra kendt fødivers",
   "directory.local": "Kun fra {domain}",
   "directory.new_arrivals": "Nye ankomster",
   "directory.recently_active": "Aktive for nyligt",
@@ -250,10 +253,10 @@
   "disabled_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret.",
   "dismissable_banner.community_timeline": "Disse er de seneste offentlige indlæg fra personer med konti hostet af {domain}.",
   "dismissable_banner.dismiss": "Afvis",
-  "dismissable_banner.explore_links": "Disse nyhedshistorier deles mest på fediverset i dag. Nyere nyhedshistorier lagt op af flere forskellige personer rangeres højere.",
-  "dismissable_banner.explore_statuses": "Disse indlæg på tværs af fediverset opnår momentum i dag. Nyere indlæg med flere boosts og favoritter rangeres højere.",
-  "dismissable_banner.explore_tags": "Disse hashtags opnår momentum på fediverset i dag. Hashtags brugt af flere forskellige personer rangeres højere.",
-  "dismissable_banner.public_timeline": "Dette er de seneste offentlige indlæg fra personer på fediverset, som folk på {domain} følger.",
+  "dismissable_banner.explore_links": "Disse nyhedshistorier deles mest på fødiverset i dag. Nyere nyhedshistorier lagt op af flere forskellige personer rangeres højere.",
+  "dismissable_banner.explore_statuses": "Disse indlæg på tværs af fødiverset opnår momentum i dag. Nyere indlæg med flere fremhævninger og favoritmærker rangeres højere.",
+  "dismissable_banner.explore_tags": "Disse etiketter opnår momentum på fødiverset i dag. Etiketter brugt af flere forskellige personer rangeres højere.",
+  "dismissable_banner.public_timeline": "Dette er de seneste offentlige indlæg fra personer på fødiverset, som folk på {domain} følger.",
   "domain_block_modal.block": "Blokér server",
   "domain_block_modal.block_account_instead": "Blokér i stedet @{name}",
   "domain_block_modal.they_can_interact_with_old_posts": "Folk fra denne server kan interagere med de gamle indlæg.",
@@ -263,20 +266,20 @@
   "domain_block_modal.you_will_lose_num_followers": "Man vil miste {followersCount, plural, one {{followersCountDisplay} følger} other {{followersCountDisplay} følgere}} og {followingCount, plural, one {{followingCountDisplay} person, man følger} other {{followingCountDisplay} personer, man følger}}.",
   "domain_block_modal.you_will_lose_relationships": "Alle følgere og personer som følges på denne server mistes.",
   "domain_block_modal.you_wont_see_posts": "Indlæg eller notifikationer fra brugere på denne server vises ikke.",
-  "domain_pill.activitypub_lets_connect": "Det muliggør at komme i forbindelse og interagere med folk ikke kun på Mastodon, men også på tværs af forskellige sociale apps.",
-  "domain_pill.activitypub_like_language": "ActivityPub er \"sproget\", Mastodon taler med andre sociale netværk.",
+  "domain_pill.activitypub_lets_connect": "Det muliggører at forbinde og interagere med folk, ikke kun på Mastodon, men også på tværs af forskellige sociale apps.",
+  "domain_pill.activitypub_like_language": "ActivityPub er \"sproget\", som Mastodon taler med andre sociale netværk.",
   "domain_pill.server": "Server",
-  "domain_pill.their_handle": "Vedkommendes handle:",
+  "domain_pill.their_handle": "Deres greb:",
   "domain_pill.their_server": "Det digitale hjem, hvor alle indlæggene findes.",
   "domain_pill.their_username": "Entydig identifikator på denne server. Det er muligt at finde brugere med samme brugernavn på forskellige servere.",
   "domain_pill.username": "Brugernavn",
-  "domain_pill.whats_in_a_handle": "Hvad er der i et handle (@brugernavn)?",
-  "domain_pill.who_they_are": "Da et handle fortæller, hvem nogen er, og hvor de er, kan man interagere med folk på tværs af det sociale net af <button>ActivityPub-drevne platforme</button>.",
-  "domain_pill.who_you_are": "Da et handle fortæller, hvem man er, og hvor man er, kan man interagere med folk på tværs af det sociale net af <button>ActivityPub-drevne platforme</button>.",
-  "domain_pill.your_handle": "Dit handle:",
-  "domain_pill.your_server": "Dit digitale hjem, hvor alle dine indlæg lever. Synes ikke om denne? Overfør til enhver tid servere samt tilhængere også.",
+  "domain_pill.whats_in_a_handle": "Hvad er der i et greb?",
+  "domain_pill.who_they_are": "Da et greb fortæller, hvem nogen er, og hvor de er, kan man interagere med folk på tværs af det sociale net af <button>ActivityPub-drevne platforme</button>.",
+  "domain_pill.who_you_are": "Da et greb fortæller, hvem man er, og hvor man er, kan man interagere med folk på tværs af det sociale net af <button>ActivityPub-drevne platforme</button>.",
+  "domain_pill.your_handle": "Dit greb:",
+  "domain_pill.your_server": "Dit digitale hjem, hvor alle dine indlæg lever. Synes ikke om den her server? Du kan til enhver tid rykke over på en anden server og beholde dine følgere.",
   "domain_pill.your_username": "Din entydige identifikator på denne server. Det er muligt at finde brugere med samme brugernavn på forskellige servere.",
-  "embed.instructions": "Indlejr dette indlæg på dit websted ved at kopiere nedenstående kode.",
+  "embed.instructions": "Indlejr dette indlæg på din hjemmeside ved at kopiere nedenstående kode.",
   "embed.preview": "Sådan kommer det til at se ud:",
   "emoji_button.activity": "Aktivitet",
   "emoji_button.clear": "Ryd",
@@ -293,38 +296,39 @@
   "emoji_button.search_results": "Søgeresultater",
   "emoji_button.symbols": "Symboler",
   "emoji_button.travel": "Rejser og steder",
+  "empty_column.account_featured": "Denne liste er tom",
   "empty_column.account_hides_collections": "Brugeren har valgt ikke at gøre denne information tilgængelig",
   "empty_column.account_suspended": "Konto suspenderet",
-  "empty_column.account_timeline": "Ingen indlæg hér!",
+  "empty_column.account_timeline": "Ingen indlæg her!",
   "empty_column.account_unavailable": "Profil utilgængelig",
   "empty_column.blocks": "Ingen brugere blokeret endnu.",
   "empty_column.bookmarked_statuses": "Du har ingen bogmærkede indlæg endnu. Når du bogmærker ét, vil det dukke op hér.",
   "empty_column.community": "Den lokale tidslinje er tom. Skriv noget offentligt for at sætte tingene i gang!",
-  "empty_column.direct": "Der er endnu ingen private omtaler. Når en sendes eller modtages, dukker den op hér.",
+  "empty_column.direct": "Der er endnu ingen private omtaler. Når en sendes eller modtages, dukker den op her.",
   "empty_column.domain_blocks": "Ingen blokerede domæner endnu.",
   "empty_column.explore_statuses": "Ingen nye tendenser lige nu. Tjek igen senere!",
-  "empty_column.favourited_statuses": "Du har endnu ingen favoritindlæg. Når du favoritmarkerer ét, vil det dukke op hér.",
-  "empty_column.favourites": "Ingen har endnu gjort dette indlæg til favorit. Når nogen gør dét, vil det dukke op hér.",
-  "empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op hér.",
-  "empty_column.followed_tags": "Ingen hashtags følges endnu. Når det sker, vil de fremgå hér.",
-  "empty_column.hashtag": "Der er intet med dette hashtag endnu.",
-  "empty_column.home": "Din hjemmetidslinje er tom! Følg nogle personer, for at udfylde den. {suggestions}",
-  "empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af listen udgiver nye indlæg vil de fremgå hér.",
-  "empty_column.mutes": "Du har endnu ikke skjult (muted) nogle brugere.",
-  "empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jf. dine indstillinger.",
-  "empty_column.notifications": "Du har endnu ingen notifikationer. Når andre interagerer med dig, vil det fremgå hér.",
-  "empty_column.public": "Der er intet hér! Skriv noget offentligt eller følg manuelt brugere fra andre servere for at se indhold",
-  "error.unexpected_crash.explanation": "Grundet en fejl i vores kode, eller en browser-kompatibilitetsfejl, kunne siden ikke vises korrekt.",
+  "empty_column.favourited_statuses": "Du har endnu ingen favoritindlæg. Når du føjer et opslag til favoritter, vil det dukke op her.",
+  "empty_column.favourites": "Ingen har endnu føjet dette indlæg til favoritter. Når nogen gør det, vil det dukke op her.",
+  "empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.",
+  "empty_column.followed_tags": "Ingen etiketter følges endnu. Når det sker, vil de fremgå her.",
+  "empty_column.hashtag": "Der er intet med denne etiket endnu.",
+  "empty_column.home": "Din hjemmetidslinje er tom! Følg nogle personer, for at fylde den op.",
+  "empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af listen udgiver nye indlæg vil de fremgå her.",
+  "empty_column.mutes": "Du har endnu ikke skjult nogle brugere.",
+  "empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jævnfør dine indstillinger.",
+  "empty_column.notifications": "Du har endnu ingen notifikationer. Når andre interagerer med dig, vil det fremgå her.",
+  "empty_column.public": "Der er intet her! Skriv noget offentligt eller følg manuelt brugere fra andre servere for at se indhold",
+  "error.unexpected_crash.explanation": "Grundet en fejl i vores kode, eller en netlæser-kompatibilitetsfejl, kunne siden ikke vises korrekt.",
   "error.unexpected_crash.explanation_addons": "Denne side kunne ikke vises korrekt. Fejlen skyldes sandsynligvis en browsertilføjelse eller automatiske oversættelsesværktøjer.",
-  "error.unexpected_crash.next_steps": "Prøv at opfriske siden. Hjælper dette ikke, kan Mastodon muligvis stadig bruges via en anden browser eller app.",
-  "error.unexpected_crash.next_steps_addons": "Prøv at deaktivere dem og genindlæse siden. Hvis det ikke hjælper, kan Mastodon muligvis stadig bruges via en anden browser eller app.",
+  "error.unexpected_crash.next_steps": "Prøv at opfriske siden. Hjælper dette ikke, kan Mastodon muligvis stadig bruges via en anden netlæser eller app.",
+  "error.unexpected_crash.next_steps_addons": "Prøv at deaktivere dem og genindlæse siden. Hvis det ikke hjælper, kan Mastodon muligvis stadig bruges via en anden netlæser eller app.",
   "errors.unexpected_crash.copy_stacktrace": "Kopiér stacktrace til udklipsholderen",
   "errors.unexpected_crash.report_issue": "Anmeld problem",
   "explore.suggested_follows": "Personer",
   "explore.title": "Udforsk",
   "explore.trending_links": "Nyheder",
   "explore.trending_statuses": "Indlæg",
-  "explore.trending_tags": "Hashtags",
+  "explore.trending_tags": "Etiketter",
   "filter_modal.added.context_mismatch_explanation": "Denne filterkategori omfatter ikke konteksten, hvorunder dette indlæg er tilgået. Redigér filteret, hvis indlægget også ønskes filtreret i denne kontekst.",
   "filter_modal.added.context_mismatch_title": "Kontekstmisforhold!",
   "filter_modal.added.expired_explanation": "Denne filterkategori er udløbet. Ændr dens udløbsdato, for at anvende den.",
@@ -332,7 +336,7 @@
   "filter_modal.added.review_and_configure": "Gå til {settings_link} for at gennemse og yderligere opsætte denne filterkategori.",
   "filter_modal.added.review_and_configure_title": "Filterindstillinger",
   "filter_modal.added.settings_link": "indstillingsside",
-  "filter_modal.added.short_explanation": "Dette indlæg er nu føjet til flg. filterkategori: {title}.",
+  "filter_modal.added.short_explanation": "Dette indlæg er nu føjet til følgende filterkategori: {title}.",
   "filter_modal.added.title": "Filter tilføjet!",
   "filter_modal.select_filter.context_mismatch": "gælder ikke for denne kontekst",
   "filter_modal.select_filter.expired": "udløbet",
@@ -365,7 +369,7 @@
   "follow_suggestions.similar_to_recently_followed_longer": "Svarende til profiler, som for nylig er fulgt",
   "follow_suggestions.view_all": "Vis alle",
   "follow_suggestions.who_to_follow": "Hvem, som skal følges",
-  "followed_tags": "Hashtag, som følges",
+  "followed_tags": "Etiketter, som følges",
   "footer.about": "Om",
   "footer.directory": "Profiloversigt",
   "footer.get_app": "Hent appen",
@@ -377,11 +381,13 @@
   "generic.saved": "Gemt",
   "getting_started.heading": "Startmenu",
   "hashtag.admin_moderation": "Åbn modereringsbrugerflade for #{name}",
+  "hashtag.browse": "Gennemse indlæg i #{hashtag}",
+  "hashtag.browse_from_account": "Gennemse indlæg fra @{name} i #{hashtag}",
   "hashtag.column_header.tag_mode.all": "og {additional}",
   "hashtag.column_header.tag_mode.any": "eller {additional}",
   "hashtag.column_header.tag_mode.none": "uden {additional}",
   "hashtag.column_settings.select.no_options_message": "Ingen forslag fundet",
-  "hashtag.column_settings.select.placeholder": "Angiv hashtags…",
+  "hashtag.column_settings.select.placeholder": "Angiv etiketter…",
   "hashtag.column_settings.tag_mode.all": "Alle disse",
   "hashtag.column_settings.tag_mode.any": "Nogle af disse",
   "hashtag.column_settings.tag_mode.none": "Ingen af disse",
@@ -389,8 +395,9 @@
   "hashtag.counter_by_accounts": "{count, plural, one {{counter} deltager} other {{counter} deltagere}}",
   "hashtag.counter_by_uses": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}} i dag",
-  "hashtag.follow": "Følg hashtag",
-  "hashtag.unfollow": "Stop med at følge hashtag",
+  "hashtag.follow": "Følg etiket",
+  "hashtag.mute": "Tavsgør #{hashtag}",
+  "hashtag.unfollow": "Stop med at følge etiket",
   "hashtags.and_other": "…og {count, plural, one {}other {# flere}}",
   "hints.profiles.followers_may_be_missing": "Der kan mangle følgere for denne profil.",
   "hints.profiles.follows_may_be_missing": "Fulgte kan mangle for denne profil.",
@@ -400,15 +407,15 @@
   "hints.profiles.see_more_posts": "Se flere indlæg på {domain}",
   "hints.threads.replies_may_be_missing": "Der kan mangle svar fra andre servere.",
   "hints.threads.see_more": "Se flere svar på {domain}",
-  "home.column_settings.show_reblogs": "Vis boosts",
+  "home.column_settings.show_reblogs": "Vis fremhævelser",
   "home.column_settings.show_replies": "Vis svar",
   "home.hide_announcements": "Skjul bekendtgørelser",
-  "home.pending_critical_update.body": "Opdater din Mastodon-server snarest muligt!",
+  "home.pending_critical_update.body": "Opdatér venligst din Mastodon-server snarest muligt!",
   "home.pending_critical_update.link": "Se opdateringer",
   "home.pending_critical_update.title": "Kritisk sikkerhedsopdatering tilgængelig!",
   "home.show_announcements": "Vis bekendtgørelser",
   "ignore_notifications_modal.disclaimer": "Mastodon kan ikke informere brugere om, at man har ignoreret deres notifikationer. Ignorerer man notifikationer, forhindrer det ikke selve beskedafsendelsen.",
-  "ignore_notifications_modal.filter_instead": "Filtrer i stedet",
+  "ignore_notifications_modal.filter_instead": "Filtrér i stedet",
   "ignore_notifications_modal.filter_to_act_users": "Man vil stadig kunne acceptere, afvise eller anmelde brugere",
   "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrering medvirker til at undgå potentiel forvirring",
   "ignore_notifications_modal.filter_to_review_separately": "Man kan gennemgå filtrerede notifikationer separat",
@@ -420,43 +427,43 @@
   "ignore_notifications_modal.private_mentions_title": "Ignorér notifikationer fra uopfordrede Private omtaler?",
   "info_button.label": "Hjælp",
   "info_button.what_is_alt_text": "<h1>Hvad er alt-tekst?</h1> <p>Alt-tekst leverer billedbeskrivelser til folk med synsnedsættelser, lav båndbredde-forbindelser eller med ønske om ekstra kontekst.</p> <p>Tilgængelighed og forståelse kan forbedres for alle ved at skrive klar, kortfattet og objektiv alt-tekst.</p> <ul> <li>Fang vigtige elementer</li> <li>Opsummér tekst i billeder</li> <li>Brug almindelig sætningsstruktur</li> <li>Undgå overflødig information</li> <li>Fokusér på tendenser og centrale resultater i kompleks grafik (såsom diagrammer eller kort)</li> </ul>",
-  "interaction_modal.action.favourite": "For at fortsætte, skal man vælge Gør til favorit fra sin konto.",
+  "interaction_modal.action.favourite": "For at fortsætte, skal du føje til favoritter fra din konto.",
   "interaction_modal.action.follow": "For at fortsætte, skal man vælge Følg fra sin konto.",
-  "interaction_modal.action.reblog": "For at fortsætte, skal man vælge Genblog fra sin konto.",
+  "interaction_modal.action.reblog": "For at fortsætte, skal man vælge Fremhæv fra sin konto.",
   "interaction_modal.action.reply": "For at fortsætte, skal man besvar fra sin konto.",
   "interaction_modal.action.vote": "For at fortsætte, skal man stemme fra sin konto.",
   "interaction_modal.go": "Gå",
   "interaction_modal.no_account_yet": "Har endnu ingen konto?",
   "interaction_modal.on_another_server": "På en anden server",
   "interaction_modal.on_this_server": "På denne server",
-  "interaction_modal.title.favourite": "Gør {name}s indlæg til favorit",
+  "interaction_modal.title.favourite": "Føj {name}s indlæg til favoritter",
   "interaction_modal.title.follow": "Følg {name}",
-  "interaction_modal.title.reblog": "Boost {name}s indlæg",
+  "interaction_modal.title.reblog": "Fremhæv {name}s indlæg",
   "interaction_modal.title.reply": "Besvar {name}s indlæg",
-  "interaction_modal.title.vote": "Deltag i {name}s afstemning",
+  "interaction_modal.title.vote": "Stem i {name}s afstemning",
   "interaction_modal.username_prompt": "F.eks. {example}",
   "intervals.full.days": "{number, plural, one {# dag} other {# dage}}",
   "intervals.full.hours": "{number, plural, one {# time} other {# timer}}",
   "intervals.full.minutes": "{number, plural, one {# minut} other {# minutter}}",
   "keyboard_shortcuts.back": "Gå tilbage",
   "keyboard_shortcuts.blocked": "Åbn listen over blokerede brugere",
-  "keyboard_shortcuts.boost": "Boost indlæg",
+  "keyboard_shortcuts.boost": "Fremhæv indlæg",
   "keyboard_shortcuts.column": "Fokusér kolonne",
   "keyboard_shortcuts.compose": "Fokusér skriveområdet",
   "keyboard_shortcuts.description": "Beskrivelse",
   "keyboard_shortcuts.direct": "for at åbne kolonnen private omtaler",
   "keyboard_shortcuts.down": "Flyt nedad på listen",
   "keyboard_shortcuts.enter": "Åbn indlæg",
-  "keyboard_shortcuts.favourite": "Favoritmarkér indlæg",
+  "keyboard_shortcuts.favourite": "Føj indlæg til favoritter",
   "keyboard_shortcuts.favourites": "Åbn favoritlisten",
-  "keyboard_shortcuts.federated": "Åbn fælles tidslinje",
+  "keyboard_shortcuts.federated": "Åbn fødereret tidslinje",
   "keyboard_shortcuts.heading": "Tastaturgenveje",
   "keyboard_shortcuts.home": "Åbn hjemmetidslinje",
   "keyboard_shortcuts.hotkey": "Hurtigtast",
   "keyboard_shortcuts.legend": "Vis dette symbol",
   "keyboard_shortcuts.local": "Åbn lokal tidslinje",
   "keyboard_shortcuts.mention": "Omtal forfatter",
-  "keyboard_shortcuts.muted": "Åbn listen over skjulte (mutede) brugere",
+  "keyboard_shortcuts.muted": "Åbn listen over skjulte brugere",
   "keyboard_shortcuts.my_profile": "Åbn din profil",
   "keyboard_shortcuts.notifications": "for at åbne notifikationskolonnen",
   "keyboard_shortcuts.open_media": "Åbn medier",
@@ -465,9 +472,9 @@
   "keyboard_shortcuts.reply": "Besvar indlægget",
   "keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger",
   "keyboard_shortcuts.search": "Fokusér søgebjælke",
-  "keyboard_shortcuts.spoilers": "Vis/skjul CW-felt",
+  "keyboard_shortcuts.spoilers": "Vis/skjul emnefelt",
   "keyboard_shortcuts.start": "Åbn \"komme i gang\"-kolonne",
-  "keyboard_shortcuts.toggle_hidden": "Vis/skjul tekst bag CW",
+  "keyboard_shortcuts.toggle_hidden": "Vis/skjul tekst bag emnefelt",
   "keyboard_shortcuts.toggle_sensitivity": "Vis/skjul medier",
   "keyboard_shortcuts.toot": "Påbegynd nyt indlæg",
   "keyboard_shortcuts.translate": "for at oversætte et indlæg",
@@ -515,16 +522,16 @@
   "moved_to_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret, da du flyttede til {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Skjul fra notifikationer",
   "mute_modal.hide_options": "Skjul valgmuligheder",
-  "mute_modal.indefinite": "Indtil jeg fjerner tavsgørelsen",
+  "mute_modal.indefinite": "Indtil jeg vælger at se dem igen",
   "mute_modal.show_options": "Vis valgmuligheder",
-  "mute_modal.they_can_mention_and_follow": "Vedkommende kan nævne og følge dig, men vil ikke blive vist.",
-  "mute_modal.they_wont_know": "Vedkommende ser ikke den aktive tavsgørelse.",
-  "mute_modal.title": "Tavsgør bruger?",
-  "mute_modal.you_wont_see_mentions": "Indlæg, som nævner vedkommende, vises ikke.",
-  "mute_modal.you_wont_see_posts": "Vedkommende kan stadig se dine indlæg, med vedkommendes vise ikke.",
+  "mute_modal.they_can_mention_and_follow": "De kan omtale og følge dig, men du vil ikke se dem.",
+  "mute_modal.they_wont_know": "De vil ikke vide, at de er blevet skjult.",
+  "mute_modal.title": "Skjul bruger?",
+  "mute_modal.you_wont_see_mentions": "Du vil ikke se indlæg som omtaler dem.",
+  "mute_modal.you_wont_see_posts": "De kan stadig se dine indlæg, men du vil ikke se deres.",
   "navigation_bar.about": "Om",
-  "navigation_bar.administration": "Håndtering",
-  "navigation_bar.advanced_interface": "Åbn i avanceret webgrænseflade",
+  "navigation_bar.administration": "Administration",
+  "navigation_bar.advanced_interface": "Åbn i avanceret netgrænseflade",
   "navigation_bar.blocks": "Blokerede brugere",
   "navigation_bar.bookmarks": "Bogmærker",
   "navigation_bar.community_timeline": "Lokal tidslinje",
@@ -534,14 +541,14 @@
   "navigation_bar.domain_blocks": "Blokerede domæner",
   "navigation_bar.explore": "Udforsk",
   "navigation_bar.favourites": "Favoritter",
-  "navigation_bar.filters": "Skjulte ord (mutede)",
+  "navigation_bar.filters": "Skjulte ord",
   "navigation_bar.follow_requests": "Følgeanmodninger",
-  "navigation_bar.followed_tags": "Hashtag, som følges",
+  "navigation_bar.followed_tags": "Etiketter, som følges",
   "navigation_bar.follows_and_followers": "Følges og følgere",
   "navigation_bar.lists": "Lister",
   "navigation_bar.logout": "Log af",
   "navigation_bar.moderation": "Moderering",
-  "navigation_bar.mutes": "Skjulte brugere (mutede)",
+  "navigation_bar.mutes": "Skjulte brugere",
   "navigation_bar.opened_in_classic_interface": "Indlæg, konti og visse andre sider åbnes som standard i den klassiske webgrænseflade.",
   "navigation_bar.personal": "Personlig",
   "navigation_bar.pins": "Fastgjorte indlæg",
@@ -559,10 +566,10 @@
   "notification.admin.sign_up.name_and_others": "{name} og {count, plural, one {# anden} other {# andre}} tilmeldte sig",
   "notification.annual_report.message": "{year} #Wrapstodon venter! Afslør årets højdepunkter og mindeværdige øjeblikke på Mastodon!",
   "notification.annual_report.view": "Vis #Wrapstodon",
-  "notification.favourite": "{name} favoritmarkerede dit indlæg",
-  "notification.favourite.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> gjorde dit indlæg til favorit",
-  "notification.favourite_pm": "{name} favoritmarkerede din private omtale",
-  "notification.favourite_pm.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> favoritmarkerede dit indlæg",
+  "notification.favourite": "{name} føjede dit indlæg til favoritter",
+  "notification.favourite.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> føjede dit indlæg til favoritter",
+  "notification.favourite_pm": "{name} føjede din private omtale til favoritter",
+  "notification.favourite_pm.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> føjede dit indlæg til favoritter",
   "notification.follow": "{name} begyndte at følge dig",
   "notification.follow.name_and_others": "{name} og <a>{count, plural, one {# andre} other {# andre}}</a> begyndte at følge dig",
   "notification.follow_request": "{name} har anmodet om at følge dig",
@@ -570,36 +577,36 @@
   "notification.label.mention": "Omtale",
   "notification.label.private_mention": "Privat omtale",
   "notification.label.private_reply": "Privat svar",
-  "notification.label.reply": "Besvar",
+  "notification.label.reply": "Svar",
   "notification.mention": "Omtale",
-  "notification.mentioned_you": "{name} nævnte dig",
+  "notification.mentioned_you": "{name} omtalte dig",
   "notification.moderation-warning.learn_more": "Læs mere",
-  "notification.moderation_warning": "Du er tildelt en moderationsadvarsel",
+  "notification.moderation_warning": "Du har fået en moderationsadvarsel",
   "notification.moderation_warning.action_delete_statuses": "Nogle af dine indlæg er blevet fjernet.",
   "notification.moderation_warning.action_disable": "Din konto er blevet deaktiveret.",
-  "notification.moderation_warning.action_mark_statuses_as_sensitive": "Nogle af dine indlæg er blevet markeret som sensitive.",
-  "notification.moderation_warning.action_none": "Din konto er tildelt en moderationsadvarsel.",
-  "notification.moderation_warning.action_sensitive": "Dine indlæg markeres fra nu af som sensitive.",
+  "notification.moderation_warning.action_mark_statuses_as_sensitive": "Nogle af dine indlæg er blevet markeret som følsomme.",
+  "notification.moderation_warning.action_none": "Din konto har fået en moderationsadvarsel.",
+  "notification.moderation_warning.action_sensitive": "Dine indlæg markeres fra nu af som følsomme.",
   "notification.moderation_warning.action_silence": "Din konto er blevet begrænset.",
   "notification.moderation_warning.action_suspend": "Din konto er suspenderet.",
   "notification.own_poll": "Din afstemning er afsluttet",
   "notification.poll": "En afstemning, hvori du har stemt, er slut",
-  "notification.reblog": "{name} boostede dit indlæg",
-  "notification.reblog.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> boostede dit indlæg",
+  "notification.reblog": "{name} fremhævede dit indlæg",
+  "notification.reblog.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> fremhævede dit indlæg",
   "notification.relationships_severance_event": "Mistede forbindelser med {name}",
-  "notification.relationships_severance_event.account_suspension": "En admin fra {from} har suspenderet {target}, hvofor opdateringer herfra eller interaktion hermed ikke længer er mulig.",
-  "notification.relationships_severance_event.domain_block": "En admin fra {from} har blokeret {target}, herunder {followersCount} tilhængere og {followingCount, plural, one {# konto, der} other {# konti, som}} følges.",
+  "notification.relationships_severance_event.account_suspension": "En admin fra {from} har suspenderet {target}, så du kan ikke længere få opdateringer fra eller interagere med dem.",
+  "notification.relationships_severance_event.domain_block": "En admin fra {from} har blokeret {target}, herunder {followersCount} følgere og {followingCount, plural, one {# konto, der} other {# konti, som}} som du følger.",
   "notification.relationships_severance_event.learn_more": "Læs mere",
-  "notification.relationships_severance_event.user_domain_block": "{target} er blevet blokeret, og {followersCount} tilhængere samt {followingCount, plural, one {# konto, der} other {# konti, som}} følges, er hermed fjernet.",
-  "notification.status": "{name} har netop postet",
+  "notification.relationships_severance_event.user_domain_block": "Du har blokeret {target}. {followersCount} af dine følgere samt {followingCount, plural, one {# konto, der} other {# konti, som}} du følger, er hermed fjernet.",
+  "notification.status": "{name} har netop slået noget op",
   "notification.update": "{name} redigerede et indlæg",
   "notification_requests.accept": "Acceptér",
   "notification_requests.accept_multiple": "{count, plural, one {Acceptér # anmodning…} other {Acceptér # anmodninger…}}",
   "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Acceptér anmodning} other {Acceptér anmodninger}}",
-  "notification_requests.confirm_accept_multiple.message": "{count, plural, one {En notifikationsanmodning} other {# notifikationsanmodninger}} er ved at blive accepteret. Fortsæt, sikker?",
+  "notification_requests.confirm_accept_multiple.message": "{count, plural, one {En notifikationsanmodning} other {# notifikationsanmodninger}} er ved at blive accepteret. Er du sikker på, at du vil fortsætte?",
   "notification_requests.confirm_accept_multiple.title": "Acceptér notifikationsanmodninger?",
   "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Afvis anmodning} other {Afvis anmodninger}}",
-  "notification_requests.confirm_dismiss_multiple.message": "{count, plural, one {En notifikationsanmodning} other {# notifikationsanmodninger}} er ved at blive afvist, hvorfor man ikke nemt vil kunne tilgå {count, plural, one {den} other {dem}} igen. Fortsæt, sikker?",
+  "notification_requests.confirm_dismiss_multiple.message": "{count, plural, one {En notifikationsanmodning} other {# notifikationsanmodninger}} er ved at blive afvist, hvorfor man ikke nemt vil kunne tilgå {count, plural, one {den} other {dem}} igen. Er du sikker på, at du vil fortsætte?",
   "notification_requests.confirm_dismiss_multiple.title": "Afvis notifikationsanmodninger?",
   "notification_requests.dismiss": "Afvis",
   "notification_requests.dismiss_multiple": "{count, plural, one {Afvis # anmodning…} other {Afvis # anmodninger…}}",
@@ -615,7 +622,7 @@
   "notifications.clear": "Ryd notifikationer",
   "notifications.clear_confirmation": "Er du sikker på, at du vil rydde alle dine notifikationer permanent?",
   "notifications.clear_title": "Ryd notifikationer?",
-  "notifications.column_settings.admin.report": "Nye anmeldelser:",
+  "notifications.column_settings.admin.report": "Nye rapporteringer:",
   "notifications.column_settings.admin.sign_up": "Nye tilmeldinger:",
   "notifications.column_settings.alert": "Computernotifikationer",
   "notifications.column_settings.favourite": "Favoritter:",
@@ -627,7 +634,7 @@
   "notifications.column_settings.mention": "Omtaler:",
   "notifications.column_settings.poll": "Afstemningsresultater:",
   "notifications.column_settings.push": "Push-notifikationer",
-  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.reblog": "Fremhævelser:",
   "notifications.column_settings.show": "Vis i kolonne",
   "notifications.column_settings.sound": "Afspil lyd",
   "notifications.column_settings.status": "Nye indlæg:",
@@ -635,7 +642,7 @@
   "notifications.column_settings.unread_notifications.highlight": "Fremhæv ulæste notifikationer",
   "notifications.column_settings.update": "Redigeringer:",
   "notifications.filter.all": "Alle",
-  "notifications.filter.boosts": "Boosts",
+  "notifications.filter.boosts": "Fremhævelser",
   "notifications.filter.favourites": "Favoritter",
   "notifications.filter.follows": "Følger",
   "notifications.filter.mentions": "Omtaler",
@@ -644,8 +651,8 @@
   "notifications.grant_permission": "Tildel tilladelse.",
   "notifications.group": "{count} notifikationer",
   "notifications.mark_as_read": "Markér alle notifikationer som læst",
-  "notifications.permission_denied": "Computernotifikationer er utilgængelige grundet tidligere afvist browsertilladelsesanmodning",
-  "notifications.permission_denied_alert": "Computernotifikationer kan ikke aktiveres, da browsertilladelse tidligere blev nægtet",
+  "notifications.permission_denied": "Computernotifikationer er utilgængelige grundet tidligere afvist netlæser-tilladelsesanmodning",
+  "notifications.permission_denied_alert": "Computernotifikationer kan ikke aktiveres, da netlæser-tilladelse tidligere blev nægtet",
   "notifications.permission_required": "Computernotifikationer er utilgængelige, da den krævede tilladelse ikke er tildelt.",
   "notifications.policy.accept": "Acceptér",
   "notifications.policy.accept_hint": "Vis notifikationer",
@@ -674,14 +681,14 @@
   "onboarding.follows.title": "Følg folk for at komme i gang",
   "onboarding.profile.discoverable": "Gør min profil synlig",
   "onboarding.profile.discoverable_hint": "Når man vælger at være synlig på Mastodon, kan ens indlæg fremgå i søgeresultater og tendenser, og profilen kan blive foreslået til andre med tilsvarende interesse.",
-  "onboarding.profile.display_name": "Visningsnavn",
-  "onboarding.profile.display_name_hint": "Fulde navn eller dit sjove navn…",
+  "onboarding.profile.display_name": "Vist navn",
+  "onboarding.profile.display_name_hint": "Dit fulde navn eller dit sjove navn…",
   "onboarding.profile.note": "Bio",
-  "onboarding.profile.note_hint": "Man kan @omtale andre personer eller #hashtags…",
+  "onboarding.profile.note_hint": "Man kan @omtale andre personer eller #etiketter…",
   "onboarding.profile.save_and_continue": "Gem og fortsæt",
   "onboarding.profile.title": "Profilopsætning",
   "onboarding.profile.upload_avatar": "Upload profilbillede",
-  "onboarding.profile.upload_header": "Upload profiloverskrift",
+  "onboarding.profile.upload_header": "Upload profilbanner",
   "password_confirmation.exceeds_maxlength": "Adgangskodebekræftelse overstiger maks. adgangskodelængde",
   "password_confirmation.mismatching": "Adgangskodebekræftelse matcher ikke",
   "picture_in_picture.restore": "Indsæt det igen",
@@ -696,21 +703,21 @@
   "poll_button.add_poll": "Tilføj en afstemning",
   "poll_button.remove_poll": "Fjern afstemning",
   "privacy.change": "Tilpas indlægsfortrolighed",
-  "privacy.direct.long": "Alle nævnt i indlægget",
-  "privacy.direct.short": "Bestemte personer",
+  "privacy.direct.long": "Alle omtalt i indlægget",
+  "privacy.direct.short": "Privat omtale",
   "privacy.private.long": "Kun dine følgere",
   "privacy.private.short": "Følgere",
   "privacy.public.long": "Alle på og udenfor Mastodon",
   "privacy.public.short": "Offentlig",
-  "privacy.unlisted.additional": "Dette er præcis som offentlig adfærd, dog vises indlægget ikke i live feeds/hashtags, udforsk eller Mastodon-søgning, selv hvis valget gælder hele kontoen.",
+  "privacy.unlisted.additional": "Dette er præcis som offentlig adfærd, dog vises indlægget ikke i tidslinjer, under etiketter, udforsk eller Mastodon-søgning, selv hvis du ellers har sagt at dine opslag godt må være søgbare.",
   "privacy.unlisted.long": "Færre algoritmiske fanfarer",
-  "privacy.unlisted.short": "Tavsgøre offentligt",
+  "privacy.unlisted.short": "Stille offentligt",
   "privacy_policy.last_updated": "Senest opdateret {date}",
   "privacy_policy.title": "Privatlivspolitik",
   "recommended": "Anbefalet",
   "refresh": "Genindlæs",
   "regeneration_indicator.please_stand_by": "Vent venligst.",
-  "regeneration_indicator.preparing_your_home_feed": "Forbereder hjemme-feed'et…",
+  "regeneration_indicator.preparing_your_home_feed": "Forbereder hjemmestrømmen…",
   "relative_time.days": "{number}d",
   "relative_time.full.days": "{number, plural, one {# dag} other {# dage}} siden",
   "relative_time.full.hours": "{number, plural, one {# time} other {# timer}} siden",
@@ -739,12 +746,12 @@
   "report.comment.title": "Er der andet, som vi bør vide?",
   "report.forward": "Videresend til {target}",
   "report.forward_hint": "Kontoen er fra en anden server. Send også en anonymiseret kopi af anmeldelsen dertil?",
-  "report.mute": "Skjul (mute)",
-  "report.mute_explanation": "Du vil ikke se vedkommendes indlæg. Vedkommende kan stadig se dine indlæg og følge dig. Vedkommende vil ikke kunne se, at de er blevet skjult.",
+  "report.mute": "Skjul",
+  "report.mute_explanation": "Du vil ikke se deres indlæg. De kan stadig se dine indlæg og følge dig. De vil ikke kunne se, at de er blevet skjult.",
   "report.next": "Næste",
   "report.placeholder": "Yderligere kommentarer",
   "report.reasons.dislike": "Jeg bryder mig ikke om det",
-  "report.reasons.dislike_description": "Det er ikke noget, man ønsker at se",
+  "report.reasons.dislike_description": "Det er ikke noget, du ønsker at se",
   "report.reasons.legal": "Det er ulovligt",
   "report.reasons.legal_description": "Du mener, at det er i strid med lovgivningen i dit eller serverens land",
   "report.reasons.other": "Det er noget andet",
@@ -764,7 +771,7 @@
   "report.thanks.title": "Ønsker ikke at se dette?",
   "report.thanks.title_actionable": "Tak for anmeldelsen, der vil blive set nærmere på dette.",
   "report.unfollow": "Følg ikke længere @{name}",
-  "report.unfollow_explanation": "Du følger denne konto. For ikke længere at se vedkommendes indlæg i dit hjemmefeed, kan du stoppe med at følge dem.",
+  "report.unfollow_explanation": "Du følger denne konto. For ikke længere at se vedkommendes indlæg i din hjemmestrøm, kan du stoppe med at følge dem.",
   "report_notification.attached_statuses": "{count, plural, one {{count} post} other {{count} poster}} vedhæftet",
   "report_notification.categories.legal": "Juridisk",
   "report_notification.categories.legal_sentence": "ikke-tilladt indhold",
@@ -779,7 +786,7 @@
   "search.placeholder": "Søg",
   "search.quick_action.account_search": "Profiler matchende {x}",
   "search.quick_action.go_to_account": "Gå til profilen {x}",
-  "search.quick_action.go_to_hashtag": "Gå til hashtagget {x}",
+  "search.quick_action.go_to_hashtag": "Gå til etiketten {x}",
   "search.quick_action.open_url": "Åbn URL i Mastodon",
   "search.quick_action.status_search": "Indlæg matchende {x}",
   "search.search_or_paste": "Søg efter eller angiv URL",
@@ -793,19 +800,19 @@
   "search_popout.user": "bruger",
   "search_results.accounts": "Profiler",
   "search_results.all": "Alle",
-  "search_results.hashtags": "Hashtags",
+  "search_results.hashtags": "Etiketter",
   "search_results.no_results": "Ingen resultater.",
-  "search_results.no_search_yet": "Prøv at søge efter indlæg, profiler eller hashtags.",
+  "search_results.no_search_yet": "Prøv at søge efter indlæg, profiler eller etiketter.",
   "search_results.see_all": "Vis alle",
   "search_results.statuses": "Indlæg",
   "search_results.title": "Søg efter \"{q}\"",
   "server_banner.about_active_users": "Folk, som brugte denne server de seneste 30 dage (månedlige aktive brugere)",
   "server_banner.active_users": "aktive brugere",
   "server_banner.administered_by": "Håndteres af:",
-  "server_banner.is_one_of_many": "{domain} er en af de mange uafhængige Mastodon-servere, man kan bruge for at deltage i fediverset.",
+  "server_banner.is_one_of_many": "{domain} er en af de mange uafhængige Mastodon-servere, man kan bruge for at deltage i fødiverset.",
   "server_banner.server_stats": "Serverstatstik:",
   "sign_in_banner.create_account": "Opret konto",
-  "sign_in_banner.follow_anyone": "Følg alle på tværs af fediverset og se alt i kronologisk rækkefølge. Ingen algoritmer, annoncer eller clickbait i syne.",
+  "sign_in_banner.follow_anyone": "Følg alle på tværs af fødiverset og se alt i kronologisk rækkefølge. Ingen algoritmer, annoncer eller clickbait i syne.",
   "sign_in_banner.mastodon_is": "Mastodon er den bedste måde at holde sig ajour med, hvad der sker.",
   "sign_in_banner.sign_in": "Log ind",
   "sign_in_banner.sso_redirect": "Log ind eller Tilmeld",
@@ -814,7 +821,7 @@
   "status.admin_status": "Åbn dette indlæg i modereringsbrugerfladen",
   "status.block": "Blokér @{name}",
   "status.bookmark": "Bogmærk",
-  "status.cancel_reblog_private": "Fjern boost",
+  "status.cancel_reblog_private": "Fjern fremhævelse",
   "status.cannot_reblog": "Dette indlæg kan ikke fremhæves",
   "status.continued_thread": "Fortsat tråd",
   "status.copy": "Kopiér link til indlæg",
@@ -837,24 +844,24 @@
   "status.media_hidden": "Medie skjult",
   "status.mention": "Nævn @{name}",
   "status.more": "Mere",
-  "status.mute": "Skjul @{name} (mute)",
-  "status.mute_conversation": "Skjul samtale (mute)",
+  "status.mute": "Skjul @{name}",
+  "status.mute_conversation": "Skjul samtale",
   "status.open": "Udvid dette indlæg",
   "status.pin": "Fastgør til profil",
   "status.pinned": "Fastgjort indlæg",
   "status.read_more": "Læs mere",
   "status.reblog": "Fremhæv",
-  "status.reblog_private": "Boost med oprindelig synlighed",
+  "status.reblog_private": "Fremhæv med oprindelig synlighed",
   "status.reblogged_by": "{name} fremhævede",
-  "status.reblogs": "{count, plural, one {# boost} other {# boosts}}",
+  "status.reblogs": "{count, plural, one {# fremhævelse} other {# fremhævelser}}",
   "status.reblogs.empty": "Ingen har endnu fremhævet dette indlæg. Når nogen gør, vil det fremgå hér.",
   "status.redraft": "Slet og omformulér",
   "status.remove_bookmark": "Fjern bogmærke",
   "status.remove_favourite": "Fjern fra favoritter",
   "status.replied_in_thread": "Svaret i tråd",
-  "status.replied_to": "Besvarede {name}",
+  "status.replied_to": "Svarede {name}",
   "status.reply": "Besvar",
-  "status.replyAll": "Besvar alle",
+  "status.replyAll": "Svar alle",
   "status.report": "Anmeld @{name}",
   "status.sensitive_warning": "Følsomt indhold",
   "status.share": "Del",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Skift abonnementssprog for {target}",
   "tabs_bar.home": "Hjem",
   "tabs_bar.notifications": "Notifikationer",
+  "terms_of_service.effective_as_of": "Gældende pr. {date}",
   "terms_of_service.title": "Tjenestevilkår",
+  "terms_of_service.upcoming_changes_on": "Kommende ændringer pr. {date}",
   "time_remaining.days": "{number, plural, one {# dag} other {# dage}} tilbage",
   "time_remaining.hours": "{number, plural, one {# time} other {# timer}} tilbage",
   "time_remaining.minutes": "{number, plural, one {# minut} other {# minutter}} tilbage",
@@ -903,8 +912,12 @@
   "video.expand": "Udvid video",
   "video.fullscreen": "Fuldskærm",
   "video.hide": "Skjul video",
-  "video.mute": "Sluk lyden",
-  "video.pause": "Pausér",
+  "video.mute": "Slå lyd fra",
+  "video.pause": "Sæt på pause",
   "video.play": "Afspil",
-  "video.unmute": "Tænd for lyden"
+  "video.skip_backward": "Overspring baglæns",
+  "video.skip_forward": "Overspring fremad",
+  "video.unmute": "Slå lyd tl",
+  "video.volume_down": "Lydstyrke ned",
+  "video.volume_up": "Lydstyrke op"
 }
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index e7499cac8d..0d5f2f2104 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Profil bearbeiten",
   "account.enable_notifications": "Benachrichtige mich wenn @{name} etwas postet",
   "account.endorse": "Im Profil empfehlen",
+  "account.featured": "Empfohlen",
+  "account.featured.hashtags": "Hashtags",
+  "account.featured.posts": "Beiträge",
   "account.featured_tags.last_status_at": "Letzter Beitrag am {date}",
   "account.featured_tags.last_status_never": "Keine Beiträge",
-  "account.featured_tags.title": "Von {name} vorgestellte Hashtags",
   "account.follow": "Folgen",
   "account.follow_back": "Ebenfalls folgen",
   "account.followers": "Follower",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
   "account.unblock": "{name} nicht mehr blockieren",
   "account.unblock_domain": "Blockierung von {domain} aufheben",
+  "account.unblock_domain_short": "Entsperren",
   "account.unblock_short": "Blockierung aufheben",
   "account.unendorse": "Im Profil nicht mehr empfehlen",
   "account.unfollow": "Entfolgen",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Suchergebnisse",
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Reisen & Orte",
+  "empty_column.account_featured": "Diese Liste ist leer",
   "empty_column.account_hides_collections": "Das Konto hat sich dazu entschieden, diese Information nicht zu veröffentlichen",
   "empty_column.account_suspended": "Konto gesperrt",
   "empty_column.account_timeline": "Keine Beiträge vorhanden!",
@@ -377,6 +381,8 @@
   "generic.saved": "Gespeichert",
   "getting_started.heading": "Auf gehts!",
   "hashtag.admin_moderation": "#{name} moderieren",
+  "hashtag.browse": "Beiträge mit #{hashtag} suchen",
+  "hashtag.browse_from_account": "Beiträge von @{name} mit #{hashtag} suchen",
   "hashtag.column_header.tag_mode.all": "und {additional}",
   "hashtag.column_header.tag_mode.any": "oder {additional}",
   "hashtag.column_header.tag_mode.none": "ohne {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}} heute",
   "hashtag.follow": "Hashtag folgen",
+  "hashtag.mute": "#{hashtag} stummschalten",
   "hashtag.unfollow": "Hashtag entfolgen",
   "hashtags.and_other": "… und {count, plural, one{# weiterer} other {# weitere}}",
   "hints.profiles.followers_may_be_missing": "Möglicherweise werden für dieses Profil nicht alle Follower angezeigt.",
@@ -591,7 +598,7 @@
   "notification.relationships_severance_event.domain_block": "Ein Admin von {from} hat {target} blockiert – darunter {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst.",
   "notification.relationships_severance_event.learn_more": "Mehr erfahren",
   "notification.relationships_severance_event.user_domain_block": "Du hast {target} blockiert – {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst, wurden entfernt.",
-  "notification.status": "{name} veröffentlichte gerade",
+  "notification.status": "{name} postete …",
   "notification.update": "{name} bearbeitete einen Beitrag",
   "notification_requests.accept": "Genehmigen",
   "notification_requests.accept_multiple": "{count, plural, one {# Anfrage genehmigen …} other {# Anfragen genehmigen …}}",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Umfrage entfernen",
   "privacy.change": "Sichtbarkeit anpassen",
   "privacy.direct.long": "Alle in diesem Beitrag erwähnten Profile",
-  "privacy.direct.short": "Ausgewählte Profile",
+  "privacy.direct.short": "Private Erwähnung",
   "privacy.private.long": "Nur deine Follower",
   "privacy.private.short": "Follower",
   "privacy.public.long": "Alle in und außerhalb von Mastodon",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Abonnierte Sprachen für {target} ändern",
   "tabs_bar.home": "Startseite",
   "tabs_bar.notifications": "Benachrichtigungen",
+  "terms_of_service.effective_as_of": "Gültig ab {date}",
   "terms_of_service.title": "Nutzungsbedingungen",
+  "terms_of_service.upcoming_changes_on": "Anstehende Änderungen am {date}",
   "time_remaining.days": "noch {number, plural, one {# Tag} other {# Tage}}",
   "time_remaining.hours": "noch {number, plural, one {# Stunde} other {# Stunden}}",
   "time_remaining.minutes": "noch {number, plural, one {# Minute} other {# Minuten}}",
@@ -906,5 +915,9 @@
   "video.mute": "Stummschalten",
   "video.pause": "Pausieren",
   "video.play": "Abspielen",
-  "video.unmute": "Stummschaltung aufheben"
+  "video.skip_backward": "Zurückspulen",
+  "video.skip_forward": "Vorspulen",
+  "video.unmute": "Stummschaltung aufheben",
+  "video.volume_down": "Leiser",
+  "video.volume_up": "Lauter"
 }
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index e45f973614..0b9e42cbe9 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -29,7 +29,6 @@
   "account.endorse": "Προβολή στο προφίλ",
   "account.featured_tags.last_status_at": "Τελευταία ανάρτηση στις {date}",
   "account.featured_tags.last_status_never": "Καμία ανάρτηση",
-  "account.featured_tags.title": "προβεβλημένες ετικέτες του/της {name}",
   "account.follow": "Ακολούθησε",
   "account.follow_back": "Ακολούθησε και εσύ",
   "account.followers": "Ακόλουθοι",
@@ -218,6 +217,10 @@
   "confirmations.logout.confirm": "Αποσύνδεση",
   "confirmations.logout.message": "Σίγουρα θέλεις να αποσυνδεθείς;",
   "confirmations.logout.title": "Αποσύνδεση;",
+  "confirmations.missing_alt_text.confirm": "Προσθήκη εναλ κειμένου",
+  "confirmations.missing_alt_text.message": "Η ανάρτησή σου περιέχει πολυμέσα χωρίς εναλλακτικό κείμενο. Η προσθήκη περιγραφών βοηθά να γίνει το περιεχόμενό σου προσβάσιμο σε περισσότερους ανθρώπους.",
+  "confirmations.missing_alt_text.secondary": "Δημοσίευση όπως και να ΄χει",
+  "confirmations.missing_alt_text.title": "Προσθήκη alt κειμένου;",
   "confirmations.mute.confirm": "Αποσιώπηση",
   "confirmations.redraft.confirm": "Διαγραφή & ξαναγράψιμο",
   "confirmations.redraft.message": "Σίγουρα θέλεις να σβήσεις αυτή την ανάρτηση και να την ξαναγράψεις; Οι προτιμήσεις και προωθήσεις θα χαθούν και οι απαντήσεις στην αρχική ανάρτηση θα μείνουν ορφανές.",
@@ -693,7 +696,7 @@
   "poll_button.remove_poll": "Αφαίρεση δημοσκόπησης",
   "privacy.change": "Προσαρμογή ιδιωτικότητας ανάρτησης",
   "privacy.direct.long": "Όλοι όσοι αναφέρθηκαν στη δημοσίευση",
-  "privacy.direct.short": "Συγκεκριμένα άτομα",
+  "privacy.direct.short": "Ιδιωτική επισήμανση",
   "privacy.private.long": "Μόνο οι ακόλουθοί σας",
   "privacy.private.short": "Ακόλουθοι",
   "privacy.public.long": "Όλοι εντός και εκτός του Mastodon",
@@ -899,8 +902,6 @@
   "video.expand": "Επέκταση βίντεο",
   "video.fullscreen": "Πλήρης οθόνη",
   "video.hide": "Απόκρυψη βίντεο",
-  "video.mute": "Σίγαση ήχου",
   "video.pause": "Παύση",
-  "video.play": "Αναπαραγωγή",
-  "video.unmute": "Αναπαραγωγή ήχου"
+  "video.play": "Αναπαραγωγή"
 }
diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json
index 70a194e1fb..b46d02baa9 100644
--- a/app/javascript/mastodon/locales/en-GB.json
+++ b/app/javascript/mastodon/locales/en-GB.json
@@ -29,7 +29,6 @@
   "account.endorse": "Feature on profile",
   "account.featured_tags.last_status_at": "Last post on {date}",
   "account.featured_tags.last_status_never": "No posts",
-  "account.featured_tags.title": "{name}'s featured hashtags",
   "account.follow": "Follow",
   "account.follow_back": "Follow back",
   "account.followers": "Followers",
@@ -218,6 +217,10 @@
   "confirmations.logout.confirm": "Log out",
   "confirmations.logout.message": "Are you sure you want to log out?",
   "confirmations.logout.title": "Log out?",
+  "confirmations.missing_alt_text.confirm": "Add alt text",
+  "confirmations.missing_alt_text.message": "Your post contains media without alt text. Adding descriptions helps make your content accessible to more people.",
+  "confirmations.missing_alt_text.secondary": "Post anyway",
+  "confirmations.missing_alt_text.title": "Add alt text?",
   "confirmations.mute.confirm": "Mute",
   "confirmations.redraft.confirm": "Delete & redraft",
   "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
@@ -693,7 +696,7 @@
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Change post privacy",
   "privacy.direct.long": "Everyone mentioned in the post",
-  "privacy.direct.short": "Specific people",
+  "privacy.direct.short": "Private mention",
   "privacy.private.long": "Only your followers",
   "privacy.private.short": "Followers",
   "privacy.public.long": "Anyone on and off Mastodon",
@@ -899,8 +902,6 @@
   "video.expand": "Expand video",
   "video.fullscreen": "Full screen",
   "video.hide": "Hide video",
-  "video.mute": "Mute sound",
   "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.play": "Play"
 }
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 7720f6cbc7..cccae96dad 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -16,7 +16,7 @@
   "about.kmyblue_capabilities": "Features available in this server",
   "about.kmyblue_capability": "This server is using kmyblue, a fork of Mastodon. On this server, kmyblues unique features are configured as follows.",
   "about.not_available": "This information has not been made available on this server.",
-  "about.powered_by": "Decentralized social media powered by {mastodon}",
+  "about.powered_by": "Decentralized social media powered by {domain}",
   "about.public_visibility": "Public visibility",
   "about.rules": "Server rules",
   "account.account_note_header": "Personal note",
@@ -38,9 +38,11 @@
   "account.edit_profile": "Edit profile",
   "account.enable_notifications": "Notify me when @{name} posts",
   "account.endorse": "Feature on profile",
+  "account.featured": "Featured",
+  "account.featured.hashtags": "Hashtags",
+  "account.featured.posts": "Posts",
   "account.featured_tags.last_status_at": "Last post on {date}",
   "account.featured_tags.last_status_never": "No posts",
-  "account.featured_tags.title": "{name}'s featured hashtags",
   "account.follow": "Follow",
   "account.follow_back": "Follow back",
   "account.followers": "Followers",
@@ -77,6 +79,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}",
   "account.unblock": "Unblock @{name}",
   "account.unblock_domain": "Unblock domain {domain}",
+  "account.unblock_domain_short": "Unblock",
   "account.unblock_short": "Unblock",
   "account.unendorse": "Don't feature on profile",
   "account.unfollow": "Unfollow",
@@ -432,6 +435,7 @@
   "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
+  "empty_column.account_featured": "This list is empty",
   "empty_column.account_hides_collections": "This user has chosen to not make this information available",
   "empty_column.account_suspended": "Account suspended",
   "empty_column.account_timeline": "No posts here!",
@@ -522,6 +526,8 @@
   "generic.saved": "Saved",
   "getting_started.heading": "Getting started",
   "hashtag.admin_moderation": "Open moderation interface for #{name}",
+  "hashtag.browse": "Browse posts in #{hashtag}",
+  "hashtag.browse_from_account": "Browse posts from @{name} in #{hashtag}",
   "hashtag.column_header.tag_mode.all": "and {additional}",
   "hashtag.column_header.tag_mode.any": "or {additional}",
   "hashtag.column_header.tag_mode.none": "without {additional}",
@@ -535,6 +541,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} posts}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today",
   "hashtag.follow": "Follow hashtag",
+  "hashtag.mute": "Mute #{hashtag}",
   "hashtag.unfollow": "Unfollow hashtag",
   "hashtags.and_other": "…and {count, plural, other {# more}}",
   "hints.profiles.followers_may_be_missing": "Followers for this profile may be missing.",
@@ -870,7 +877,7 @@
   "privacy.circle.long": "Circle members only",
   "privacy.circle.short": "Circle",
   "privacy.direct.long": "Everyone mentioned in the post",
-  "privacy.direct.short": "Specific people",
+  "privacy.direct.short": "Private mention",
   "privacy.limited.short": "Limited",
   "privacy.login.long": "Visible for login users only",
   "privacy.login.short": "Login only",
@@ -1088,7 +1095,9 @@
   "subscribed_languages.target": "Change subscribed languages for {target}",
   "tabs_bar.home": "Home",
   "tabs_bar.notifications": "Notifications",
+  "terms_of_service.effective_as_of": "Effective as of {date}",
   "terms_of_service.title": "Terms of Service",
+  "terms_of_service.upcoming_changes_on": "Upcoming changes on {date}",
   "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
   "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
   "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
@@ -1119,8 +1128,12 @@
   "video.expand": "Expand video",
   "video.fullscreen": "Full screen",
   "video.hide": "Hide video",
-  "video.mute": "Mute sound",
+  "video.mute": "Mute",
   "video.pause": "Pause",
   "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.skip_backward": "Skip backward",
+  "video.skip_forward": "Skip forward",
+  "video.unmute": "Unmute",
+  "video.volume_down": "Volume down",
+  "video.volume_up": "Volume up"
 }
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 7da4102920..1d360e59d7 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -1,5 +1,5 @@
 {
-  "about.blocks": "Administritaj serviloj",
+  "about.blocks": "Reguligitaj serviloj",
   "about.contact": "Kontakto:",
   "about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Kialo ne disponeblas",
@@ -26,10 +26,11 @@
   "account.domain_blocked": "Domajno blokita",
   "account.edit_profile": "Redakti la profilon",
   "account.enable_notifications": "Sciigu min kiam @{name} afiŝos",
-  "account.endorse": "Rekomendi ĉe via profilo",
+  "account.endorse": "Prezenti ĉe via profilo",
+  "account.featured.hashtags": "Kradvortoj",
+  "account.featured.posts": "Afiŝoj",
   "account.featured_tags.last_status_at": "Lasta afîŝo je {date}",
   "account.featured_tags.last_status_never": "Neniu afiŝo",
-  "account.featured_tags.title": "Rekomendataj kradvortoj de {name}",
   "account.follow": "Sekvi",
   "account.follow_back": "Sekvu reen",
   "account.followers": "Sekvantoj",
@@ -45,7 +46,7 @@
   "account.languages": "Ŝanĝi la abonitajn lingvojn",
   "account.link_verified_on": "Propreco de tiu ligilo estis konfirmita je {date}",
   "account.locked_info": "Tiu konto estas privatigita. La posedanto mane akceptas tiun, kiu povas sekvi rin.",
-  "account.media": "Plurmedio",
+  "account.media": "Aŭdovidaĵoj",
   "account.mention": "Mencii @{name}",
   "account.moved_to": "{name} indikis, ke ria nova konto estas nun:",
   "account.mute": "Silentigi @{name}",
@@ -65,6 +66,7 @@
   "account.statuses_counter": "{count, plural,one {{counter} afiŝo} other {{counter} afiŝoj}}",
   "account.unblock": "Malbloki @{name}",
   "account.unblock_domain": "Malbloki la domajnon {domain}",
+  "account.unblock_domain_short": "Malbloki",
   "account.unblock_short": "Malbloki",
   "account.unendorse": "Ne plu rekomendi ĉe la profilo",
   "account.unfollow": "Ĉesi sekvi",
@@ -172,7 +174,7 @@
   "column_search.cancel": "Nuligi",
   "column_subheading.settings": "Agordoj",
   "community.column_settings.local_only": "Nur loka",
-  "community.column_settings.media_only": "Nur plurmedio",
+  "community.column_settings.media_only": "Nur vidaŭdaĵoj",
   "community.column_settings.remote_only": "Nur fora",
   "compose.language.change": "Ŝanĝi lingvon",
   "compose.language.search": "Serĉi lingvojn...",
@@ -208,7 +210,7 @@
   "confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?",
   "confirmations.delete_list.title": "Ĉu forigi liston?",
   "confirmations.discard_edit_media.confirm": "Forĵeti",
-  "confirmations.discard_edit_media.message": "Vi havas nekonservitajn ŝanĝojn de la priskribo aŭ la antaŭmontro de la plurmedio, ĉu vi forĵetu ilin malgraŭe?",
+  "confirmations.discard_edit_media.message": "Vi havas nekonservitajn ŝanĝojn de la priskribo aŭ la antaŭvidigo de la vidaŭdaĵo, ĉu vi forĵetu ilin malgraŭe?",
   "confirmations.edit.confirm": "Redakti",
   "confirmations.edit.message": "Redakti nun anstataŭigos la skribatan afiŝon. Ĉu vi certas, ke vi volas daŭrigi?",
   "confirmations.edit.title": "Ĉu superskribi afiŝon?",
@@ -219,8 +221,8 @@
   "confirmations.logout.message": "Ĉu vi certas, ke vi volas elsaluti?",
   "confirmations.logout.title": "Ĉu elsaluti?",
   "confirmations.missing_alt_text.confirm": "Aldoni alttekston",
-  "confirmations.missing_alt_text.message": "Via afiŝo enhavas amaskomunikilaron sen altteksto. Aldono de priskriboj helpas fari vian enhavon alirebla por pli da homoj.",
-  "confirmations.missing_alt_text.secondary": "Afiŝu ĉiukaze",
+  "confirmations.missing_alt_text.message": "Via afiŝo enhavas aŭdvidaĵon sen alternativa teksto. Aldono de priskriboj helpas fari vian enhavon alirebla por pli da homoj.",
+  "confirmations.missing_alt_text.secondary": "Afiŝi ĉiuokaze",
   "confirmations.missing_alt_text.title": "Ĉu aldoni alttekston?",
   "confirmations.mute.confirm": "Silentigi",
   "confirmations.redraft.confirm": "Forigi kaj reskribi",
@@ -293,6 +295,7 @@
   "emoji_button.search_results": "Serĉaj rezultoj",
   "emoji_button.symbols": "Simboloj",
   "emoji_button.travel": "Vojaĝoj kaj lokoj",
+  "empty_column.account_featured": "Ĉi tiu listo estas malplena",
   "empty_column.account_hides_collections": "Ĉi tiu uzanto elektis ne disponebligi ĉi tiu informon",
   "empty_column.account_suspended": "Konto suspendita",
   "empty_column.account_timeline": "Neniuj afiŝoj ĉi tie!",
@@ -459,7 +462,7 @@
   "keyboard_shortcuts.muted": "Malfermu la liston de silentigitaj uzantoj",
   "keyboard_shortcuts.my_profile": "Malfermu vian profilon",
   "keyboard_shortcuts.notifications": "Malfermu la sciigajn kolumnon",
-  "keyboard_shortcuts.open_media": "Malfermu plurmedion",
+  "keyboard_shortcuts.open_media": "Malfermi vidaŭdaĵon",
   "keyboard_shortcuts.pinned": "Malfermu alpinglitajn afiŝojn-liston",
   "keyboard_shortcuts.profile": "Malfermu la profilon de aŭtoroprofilo",
   "keyboard_shortcuts.reply": "Respondu al afiŝo",
@@ -468,7 +471,7 @@
   "keyboard_shortcuts.spoilers": "Montri/kaŝi CW-kampon",
   "keyboard_shortcuts.start": "Malfermu \"por komenci\" kolumnon",
   "keyboard_shortcuts.toggle_hidden": "Montri/kaŝi tekston malantaŭ CW",
-  "keyboard_shortcuts.toggle_sensitivity": "Montri/kaŝi plurmedion",
+  "keyboard_shortcuts.toggle_sensitivity": "Montri/kaŝi vidaŭdaĵojn",
   "keyboard_shortcuts.toot": "Komencu novan afiŝon",
   "keyboard_shortcuts.translate": "Traduki afiŝon",
   "keyboard_shortcuts.unfocus": "Senfokusigi verki tekstareon/serĉon",
@@ -574,11 +577,11 @@
   "notification.mention": "Mencii",
   "notification.mentioned_you": "{name} menciis vin",
   "notification.moderation-warning.learn_more": "Lerni pli",
-  "notification.moderation_warning": "Vi ricevis moderigan averton",
+  "notification.moderation_warning": "Vi ricevis reguligan averton",
   "notification.moderation_warning.action_delete_statuses": "Kelkaj el viaj afiŝoj estis forigitaj.",
   "notification.moderation_warning.action_disable": "Via konto estas malŝaltita.",
   "notification.moderation_warning.action_mark_statuses_as_sensitive": "Kelkaj el viaj afiŝoj estis markitaj kiel sentemaj.",
-  "notification.moderation_warning.action_none": "Via konto ricevis moderigan averton.",
+  "notification.moderation_warning.action_none": "Via konto ricevis reguligan averton.",
   "notification.moderation_warning.action_sensitive": "Viaj afiŝoj estos markitaj kiel sentemaj ekde nun.",
   "notification.moderation_warning.action_silence": "Via konto estis limigita.",
   "notification.moderation_warning.action_suspend": "Via konto estas malakceptita.",
@@ -605,8 +608,8 @@
   "notification_requests.dismiss_multiple": "{count, plural, one {Malakcepti # peton…} other {# Malakcepti # petojn…}}",
   "notification_requests.edit_selection": "Redakti",
   "notification_requests.exit_selection": "Farita",
-  "notification_requests.explainer_for_limited_account": "Sciigoj de ĉi tiu konto estis filtritaj ĉar la konto estis limigita de moderanto.",
-  "notification_requests.explainer_for_limited_remote_account": "Sciigoj de ĉi tiu konto estis filtritaj ĉar la konto aŭ ĝia servilo estis limigitaj de moderanto.",
+  "notification_requests.explainer_for_limited_account": "Sciigoj de ĉi tiu konto estis filtritaj ĉar la konto estis limigita de reguligisto.",
+  "notification_requests.explainer_for_limited_remote_account": "Sciigoj de ĉi tiu konto estis filtritaj ĉar la konto aŭ ĝia servilo estis limigitaj de reguligisto.",
   "notification_requests.maximize": "Maksimumigi",
   "notification_requests.minimize_banner": "Minimumigi filtritajn sciigojn-rubandon",
   "notification_requests.notifications_from": "Sciigoj de {name}",
@@ -653,8 +656,8 @@
   "notifications.policy.drop_hint": "Sendi al la malpleno, por neniam esti vidita denove",
   "notifications.policy.filter": "Filtri",
   "notifications.policy.filter_hint": "Sendi al filtritaj sciigoj-enirkesto",
-  "notifications.policy.filter_limited_accounts_hint": "Limigita de servilaj moderigantoj",
-  "notifications.policy.filter_limited_accounts_title": "Moderigitaj kontoj",
+  "notifications.policy.filter_limited_accounts_hint": "Limigita de servilaj reguligistoj",
+  "notifications.policy.filter_limited_accounts_title": "Reguligitaj kontoj",
   "notifications.policy.filter_new_accounts.hint": "Kreite en la {days, plural, one {lasta tago} other {# lastaj tagoj}}",
   "notifications.policy.filter_new_accounts_title": "Novaj kontoj",
   "notifications.policy.filter_not_followers_hint": "Inkluzive de homoj, kiuj sekvis vin malpli ol {days, plural, one {unu tago} other {# tagoj}}",
@@ -697,7 +700,7 @@
   "poll_button.remove_poll": "Forigi balotenketon",
   "privacy.change": "Ŝanĝu afiŝan privatecon",
   "privacy.direct.long": "Ĉiuj menciitaj en la afiŝo",
-  "privacy.direct.short": "Specifaj homoj",
+  "privacy.direct.short": "Privata mencio",
   "privacy.private.long": "Nur viaj sekvantoj",
   "privacy.private.short": "Sekvantoj",
   "privacy.public.long": "Ĉiujn ajn ĉe kaj ekster Mastodon",
@@ -781,7 +784,7 @@
   "search.quick_action.go_to_account": "Iri al profilo {x}",
   "search.quick_action.go_to_hashtag": "Iri al kradvorto {x}",
   "search.quick_action.open_url": "Malfermi URL en Mastodono",
-  "search.quick_action.status_search": "Afiŝoj kiuj kongruas kun {x}",
+  "search.quick_action.status_search": "Afiŝoj kiuj konformas kun {x}",
   "search.search_or_paste": "Serĉu aŭ algluu URL-on",
   "search_popout.full_text_search_disabled_message": "Ne havebla sur {domain}.",
   "search_popout.full_text_search_logged_out_message": "Disponebla nur kiam ensalutinte.",
@@ -796,7 +799,7 @@
   "search_results.hashtags": "Kradvortoj",
   "search_results.no_results": "Ne estas rezultoj.",
   "search_results.no_search_yet": "Provu serĉi afiŝojn, profilojn aŭ kradvortojn.",
-  "search_results.see_all": "Vidu ĉiujn",
+  "search_results.see_all": "Vidi ĉiujn",
   "search_results.statuses": "Afiŝoj",
   "search_results.title": "Serĉu \"{q}\"",
   "server_banner.about_active_users": "Personoj uzantaj ĉi tiun servilon dum la lastaj 30 tagoj (Aktivaj Uzantoj Monate)",
@@ -809,9 +812,9 @@
   "sign_in_banner.mastodon_is": "Mastodon estas la plej bona maniero resti ĝisdata pri aktualaĵoj.",
   "sign_in_banner.sign_in": "Ensaluti",
   "sign_in_banner.sso_redirect": "Ensalutu aŭ Registriĝi",
-  "status.admin_account": "Malfermi fasadon de moderigado por @{name}",
-  "status.admin_domain": "Malfermu moderigan interfacon por {domain}",
-  "status.admin_status": "Malfermi ĉi tiun afiŝon en la kontrola interfaco",
+  "status.admin_account": "Malfermi fasadon de la reguligado por @{name}",
+  "status.admin_domain": "Malfermi fasadon de la reguligado por {domain}",
+  "status.admin_status": "Malfermi ĉi tiun afiŝon en la fasado de la reguligado",
   "status.block": "Bloki @{name}",
   "status.bookmark": "Aldoni al la legosignoj",
   "status.cancel_reblog_private": "Ne plu diskonigi",
@@ -834,7 +837,7 @@
   "status.load_more": "Ŝargi pli",
   "status.media.open": "Alklaki por malfermi",
   "status.media.show": "Alklaki por montri",
-  "status.media_hidden": "Plurmedio kaŝita",
+  "status.media_hidden": "Vidaŭdaĵo kaŝita",
   "status.mention": "Mencii @{name}",
   "status.more": "Pli",
   "status.mute": "Silentigi @{name}",
@@ -872,7 +875,9 @@
   "subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",
   "tabs_bar.home": "Hejmo",
   "tabs_bar.notifications": "Sciigoj",
+  "terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}",
   "terms_of_service.title": "Kondiĉoj de uzado",
+  "terms_of_service.upcoming_changes_on": "Venontaj ŝanĝoj el {date}",
   "time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restas",
   "time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restas",
   "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restas",
@@ -906,5 +911,5 @@
   "video.mute": "Silentigi",
   "video.pause": "Paŭzigi",
   "video.play": "Ekigi",
-  "video.unmute": "Malsilentigi"
+  "video.unmute": "Ne plu silentigi"
 }
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index 7cdf1f2d6b..bf6d620474 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Editar perfil",
   "account.enable_notifications": "Notificarme cuando @{name} envíe mensajes",
   "account.endorse": "Destacar en el perfil",
+  "account.featured": "Destacados",
+  "account.featured.hashtags": "Etiquetas",
+  "account.featured.posts": "Mensajes",
   "account.featured_tags.last_status_at": "Último mensaje: {date}",
   "account.featured_tags.last_status_never": "Sin mensajes",
-  "account.featured_tags.title": "Etiquetas destacadas de {name}",
   "account.follow": "Seguir",
   "account.follow_back": "Seguir",
   "account.followers": "Seguidores",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
   "account.unblock": "Desbloquear a @{name}",
   "account.unblock_domain": "Desbloquear dominio {domain}",
+  "account.unblock_domain_short": "Desbloquear",
   "account.unblock_short": "Desbloquear",
   "account.unendorse": "No destacar en el perfil",
   "account.unfollow": "Dejar de seguir",
@@ -218,10 +221,10 @@
   "confirmations.logout.confirm": "Cerrar sesión",
   "confirmations.logout.message": "¿Estás seguro que querés cerrar la sesión?",
   "confirmations.logout.title": "¿Cerrar sesión?",
-  "confirmations.missing_alt_text.confirm": "Añadir texto alternativo",
-  "confirmations.missing_alt_text.message": "Tu publicación contiene medios sin texto alternativo. Añadir descripciones ayuda a que tu contenido sea accesible para más personas.",
-  "confirmations.missing_alt_text.secondary": "Publicar de todos modos",
-  "confirmations.missing_alt_text.title": "¿Deseas añadir texto alternativo?",
+  "confirmations.missing_alt_text.confirm": "Agregar texto alternativo",
+  "confirmations.missing_alt_text.message": "Tu mensaje contiene medios sin texto alternativo. Agregar descripciones ayuda a que tu contenido sea accesible para más personas.",
+  "confirmations.missing_alt_text.secondary": "Enviar de todos modos",
+  "confirmations.missing_alt_text.title": "¿Agregar texto alternativo?",
   "confirmations.mute.confirm": "Silenciar",
   "confirmations.redraft.confirm": "Eliminar mensaje original y editarlo",
   "confirmations.redraft.message": "¿Estás seguro que querés eliminar este mensaje y volver a editarlo? Se perderán las veces marcadas como favorito y sus adhesiones, y las respuestas al mensaje original quedarán huérfanas.",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Resultados de búsqueda",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viajes y lugares",
+  "empty_column.account_featured": "Esta lista está vacía",
   "empty_column.account_hides_collections": "Este usuario eligió no publicar esta información",
   "empty_column.account_suspended": "Cuenta suspendida",
   "empty_column.account_timeline": "¡No hay mensajes acá!",
@@ -377,6 +381,8 @@
   "generic.saved": "Guardado",
   "getting_started.heading": "Inicio de Mastodon",
   "hashtag.admin_moderation": "Abrir interface de moderación para #{name}",
+  "hashtag.browse": "Ver publicaciones con #{hashtag}",
+  "hashtag.browse_from_account": "Ver publicaciones de @{name} con #{hashtag}",
   "hashtag.column_header.tag_mode.all": "y {additional}",
   "hashtag.column_header.tag_mode.any": "o {additional}",
   "hashtag.column_header.tag_mode.none": "sin {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}} hoy",
   "hashtag.follow": "Seguir etiqueta",
+  "hashtag.mute": "Silenciar #{hashtag}",
   "hashtag.unfollow": "Dejar de seguir etiqueta",
   "hashtags.and_other": "…y {count, plural, other {# más}}",
   "hints.profiles.followers_may_be_missing": "Es posible que falten seguidores de este perfil.",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Quitar encuesta",
   "privacy.change": "Configurar privacidad del mensaje",
   "privacy.direct.long": "Todas las cuentas mencionadas en el mensaje",
-  "privacy.direct.short": "Cuentas específicas",
+  "privacy.direct.short": "Mención privada",
   "privacy.private.long": "Solo tus seguidores",
   "privacy.private.short": "Seguidores",
   "privacy.public.long": "Cualquier persona dentro y fuera de Mastodon",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
   "tabs_bar.home": "Principal",
   "tabs_bar.notifications": "Notificaciones",
+  "terms_of_service.effective_as_of": "Efectivo a partir de {date}",
   "terms_of_service.title": "Términos del servicio",
+  "terms_of_service.upcoming_changes_on": "Próximos cambios el {date}",
   "time_remaining.days": "{number, plural,one {queda # día} other {quedan # días}}",
   "time_remaining.hours": "{number, plural,one {queda # hora} other {quedan # horas}}",
   "time_remaining.minutes": "{number, plural,one {queda # minuto} other {quedan # minutos}}",
@@ -906,5 +915,9 @@
   "video.mute": "Silenciar",
   "video.pause": "Pausar",
   "video.play": "Reproducir",
-  "video.unmute": "Dejar de silenciar"
+  "video.skip_backward": "Saltar atrás",
+  "video.skip_forward": "Adelantar",
+  "video.unmute": "Quitar silenciado",
+  "video.volume_down": "Bajar volumen",
+  "video.volume_up": "Subir volumen"
 }
diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json
index 1f4d4546e9..69e20fc016 100644
--- a/app/javascript/mastodon/locales/es-MX.json
+++ b/app/javascript/mastodon/locales/es-MX.json
@@ -13,13 +13,13 @@
   "about.rules": "Reglas del servidor",
   "account.account_note_header": "Nota personal",
   "account.add_or_remove_from_list": "Agregar o eliminar de las listas",
-  "account.badges.bot": "Bot",
+  "account.badges.bot": "Automatizada",
   "account.badges.group": "Grupo",
   "account.block": "Bloquear a @{name}",
   "account.block_domain": "Bloquear dominio {domain}",
   "account.block_short": "Bloquear",
   "account.blocked": "Bloqueado",
-  "account.cancel_follow_request": "Retirar solicitud de seguimiento",
+  "account.cancel_follow_request": "Cancelar seguimiento",
   "account.copy": "Copiar enlace al perfil",
   "account.direct": "Mención privada @{name}",
   "account.disable_notifications": "Dejar de notificarme cuando @{name} publique algo",
@@ -27,9 +27,11 @@
   "account.edit_profile": "Editar perfil",
   "account.enable_notifications": "Notificarme cuando @{name} publique algo",
   "account.endorse": "Destacar en mi perfil",
+  "account.featured": "Destacado",
+  "account.featured.hashtags": "Etiquetas",
+  "account.featured.posts": "Publicaciones",
   "account.featured_tags.last_status_at": "Última publicación el {date}",
   "account.featured_tags.last_status_never": "Sin publicaciones",
-  "account.featured_tags.title": "Etiquetas destacadas de {name}",
   "account.follow": "Seguir",
   "account.follow_back": "Seguir también",
   "account.followers": "Seguidores",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
   "account.unblock": "Desbloquear a @{name}",
   "account.unblock_domain": "Mostrar a {domain}",
+  "account.unblock_domain_short": "Desbloquear",
   "account.unblock_short": "Desbloquear",
   "account.unendorse": "No mostrar en el perfil",
   "account.unfollow": "Dejar de seguir",
@@ -84,7 +87,7 @@
   "alert.rate_limited.message": "Por favor, intenta después de las {retry_time, time, medium}.",
   "alert.rate_limited.title": "Tarifa limitada",
   "alert.unexpected.message": "Hubo un error inesperado.",
-  "alert.unexpected.title": "¡Ups!",
+  "alert.unexpected.title": "¡Uy!",
   "alt_text_badge.title": "Texto alternativo",
   "alt_text_modal.add_alt_text": "Añadir texto alternativo",
   "alt_text_modal.add_text_from_image": "Añadir texto de la imagen",
@@ -192,10 +195,10 @@
   "compose_form.poll.switch_to_multiple": "Cambiar la encuesta para permitir múltiples opciones",
   "compose_form.poll.switch_to_single": "Cambiar la encuesta para permitir una única opción",
   "compose_form.poll.type": "Estilo",
-  "compose_form.publish": "Publicación",
-  "compose_form.publish_form": "Publicar",
+  "compose_form.publish": "Publicar",
+  "compose_form.publish_form": "Nueva publicación",
   "compose_form.reply": "Respuesta",
-  "compose_form.save_changes": "Actualización",
+  "compose_form.save_changes": "Actualizar",
   "compose_form.spoiler.marked": "Quitar advertencia de contenido",
   "compose_form.spoiler.unmarked": "Añadir advertencia de contenido",
   "compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)",
@@ -285,7 +288,7 @@
   "emoji_button.food": "Comida y bebida",
   "emoji_button.label": "Insertar emoji",
   "emoji_button.nature": "Naturaleza",
-  "emoji_button.not_found": "Sin emojis coincidentes",
+  "emoji_button.not_found": "No se han encontrado emojis que coincidan",
   "emoji_button.objects": "Objetos",
   "emoji_button.people": "Gente",
   "emoji_button.recent": "Usados frecuentemente",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Resultados de búsqueda",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viajes y lugares",
+  "empty_column.account_featured": "Esta lista está vacía",
   "empty_column.account_hides_collections": "Este usuario ha elegido no hacer disponible esta información",
   "empty_column.account_suspended": "Cuenta suspendida",
   "empty_column.account_timeline": "¡No hay publicaciones aquí!",
@@ -308,8 +312,8 @@
   "empty_column.follow_requests": "No tienes ninguna petición de seguidor. Cuando recibas una, se mostrará aquí.",
   "empty_column.followed_tags": "No estás siguiendo ninguna etiqueta todavía. Cuando lo hagas, aparecerá aquí.",
   "empty_column.hashtag": "No hay nada en esta etiqueta aún.",
-  "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
-  "empty_column.list": "No hay nada en esta lista aún. Cuando miembros de esta lista publiquen nuevos estatus, estos aparecerán qui.",
+  "empty_column.home": "¡Tu cronología está vacía! Sigue a más gente para llenarla.",
+  "empty_column.list": "Aún no hay nada en esta lista. Cuando los miembros de esta lista publiquen nuevos contenidos, aparecerán aquí.",
   "empty_column.mutes": "Aún no has silenciado a ningún usuario.",
   "empty_column.notification_requests": "¡Todo limpio! No hay nada aquí. Cuando recibas nuevas notificaciones, aparecerán aquí conforme a tu configuración.",
   "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
@@ -377,6 +381,8 @@
   "generic.saved": "Guardado",
   "getting_started.heading": "Primeros pasos",
   "hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
+  "hashtag.browse": "Explorar publicaciones en #{hashtag}",
+  "hashtag.browse_from_account": "Explorar publicaciones desde @{name} en #{hashtag}",
   "hashtag.column_header.tag_mode.all": "y {additional}",
   "hashtag.column_header.tag_mode.any": "o {additional}",
   "hashtag.column_header.tag_mode.none": "sin {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}} hoy",
   "hashtag.follow": "Seguir etiqueta",
+  "hashtag.mute": "Silenciar #{hashtag}",
   "hashtag.unfollow": "Dejar de seguir etiqueta",
   "hashtags.and_other": "…y {count, plural, other {# más}}",
   "hints.profiles.followers_may_be_missing": "Puede que no se muestren todos los seguidores de este perfil.",
@@ -438,41 +445,41 @@
   "intervals.full.days": "{number, plural, one {# día} other {# días}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
-  "keyboard_shortcuts.back": "volver atrás",
-  "keyboard_shortcuts.blocked": "abrir una lista de usuarios bloqueados",
+  "keyboard_shortcuts.back": "Volver atrás",
+  "keyboard_shortcuts.blocked": "Abrir la lista de usuarios bloqueados",
   "keyboard_shortcuts.boost": "Impulsar publicación",
-  "keyboard_shortcuts.column": "enfocar un estado en una de las columnas",
-  "keyboard_shortcuts.compose": "enfocar el área de texto de redacción",
+  "keyboard_shortcuts.column": "Enfocar columna",
+  "keyboard_shortcuts.compose": "Enfocar el área de texto de redacción",
   "keyboard_shortcuts.description": "Descripción",
   "keyboard_shortcuts.direct": "para abrir la columna de menciones privadas",
-  "keyboard_shortcuts.down": "mover hacia abajo en la lista",
+  "keyboard_shortcuts.down": "Descender en la lista",
   "keyboard_shortcuts.enter": "Abrir publicación",
   "keyboard_shortcuts.favourite": "Marcar como favorita la publicación",
   "keyboard_shortcuts.favourites": "Abrir lista de favoritos",
-  "keyboard_shortcuts.federated": "abrir el timeline federado",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.home": "abrir el timeline propio",
-  "keyboard_shortcuts.hotkey": "Tecla caliente",
-  "keyboard_shortcuts.legend": "para mostrar esta leyenda",
-  "keyboard_shortcuts.local": "abrir el timeline local",
-  "keyboard_shortcuts.mention": "para mencionar al autor",
-  "keyboard_shortcuts.muted": "abrir la lista de usuarios silenciados",
-  "keyboard_shortcuts.my_profile": "abrir tu perfil",
-  "keyboard_shortcuts.notifications": "abrir la columna de notificaciones",
-  "keyboard_shortcuts.open_media": "para abrir archivos multimedia",
+  "keyboard_shortcuts.federated": "Abrir cronología federada",
+  "keyboard_shortcuts.heading": "Atajos de teclado",
+  "keyboard_shortcuts.home": "Abrir cronología principal",
+  "keyboard_shortcuts.hotkey": "Tecla de acceso rápido",
+  "keyboard_shortcuts.legend": "Mostrar esta leyenda",
+  "keyboard_shortcuts.local": "Abrir cronología local",
+  "keyboard_shortcuts.mention": "Mencionar al autor",
+  "keyboard_shortcuts.muted": "Abrir la lista de usuarios silenciados",
+  "keyboard_shortcuts.my_profile": "Abrir tu perfil",
+  "keyboard_shortcuts.notifications": "Abrir la columna de notificaciones",
+  "keyboard_shortcuts.open_media": "Abrir multimedia",
   "keyboard_shortcuts.pinned": "Abrir la lista de publicaciones fijadas",
-  "keyboard_shortcuts.profile": "abrir el perfil del autor",
+  "keyboard_shortcuts.profile": "Abrir perfil del autor",
   "keyboard_shortcuts.reply": "Responder a la publicación",
-  "keyboard_shortcuts.requests": "abrir la lista de peticiones de seguidores",
-  "keyboard_shortcuts.search": "para poner el foco en la búsqueda",
-  "keyboard_shortcuts.spoilers": "para mostrar/ocultar el campo CW",
-  "keyboard_shortcuts.start": "abrir la columna \"comenzar\"",
-  "keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)",
-  "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios",
+  "keyboard_shortcuts.requests": "Abrir lista de solicitudes de seguimiento",
+  "keyboard_shortcuts.search": "Enfocar la barra de búsqueda",
+  "keyboard_shortcuts.spoilers": "Mostrar/ocultar el campo AC",
+  "keyboard_shortcuts.start": "Abrir la columna “empezar”",
+  "keyboard_shortcuts.toggle_hidden": "Mostrar/ocultar texto detrás de AC",
+  "keyboard_shortcuts.toggle_sensitivity": "Mostrar/ocultar multimedia",
   "keyboard_shortcuts.toot": "Comenzar una nueva publicación",
   "keyboard_shortcuts.translate": "para traducir una publicación",
-  "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
-  "keyboard_shortcuts.up": "para ir hacia arriba en la lista",
+  "keyboard_shortcuts.unfocus": "Desenfocar área de redacción/búsqueda",
+  "keyboard_shortcuts.up": "Ascender en la lista",
   "lightbox.close": "Cerrar",
   "lightbox.next": "Siguiente",
   "lightbox.previous": "Anterior",
@@ -644,7 +651,7 @@
   "notifications.grant_permission": "Conceder permiso.",
   "notifications.group": "{count} notificaciones",
   "notifications.mark_as_read": "Marcar todas las notificaciones como leídas",
-  "notifications.permission_denied": "No se pueden habilitar las notificaciones de escritorio ya que se denegó el permiso.",
+  "notifications.permission_denied": "No se pueden habilitar las notificaciones de escritorio, ya que se denegó el permiso",
   "notifications.permission_denied_alert": "No se pueden habilitar las notificaciones de escritorio, ya que el permiso del navegador fue denegado anteriormente",
   "notifications.permission_required": "Las notificaciones de escritorio no están disponibles porque no se ha concedido el permiso requerido.",
   "notifications.policy.accept": "Aceptar",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Eliminar encuesta",
   "privacy.change": "Ajustar privacidad",
   "privacy.direct.long": "Todos los mencionados en la publicación",
-  "privacy.direct.short": "Personas específicas",
+  "privacy.direct.short": "Mención privada",
   "privacy.private.long": "Sólo tus seguidores",
   "privacy.private.short": "Seguidores",
   "privacy.public.long": "Cualquiera dentro y fuera de Mastodon",
@@ -835,7 +842,7 @@
   "status.media.open": "Click para abrir",
   "status.media.show": "Click para mostrar",
   "status.media_hidden": "Contenido multimedia oculto",
-  "status.mention": "Mencionar",
+  "status.mention": "Mencionar @{name}",
   "status.more": "Más",
   "status.mute": "Silenciar @{name}",
   "status.mute_conversation": "Silenciar conversación",
@@ -855,7 +862,7 @@
   "status.replied_to": "Respondió a {name}",
   "status.reply": "Responder",
   "status.replyAll": "Responder al hilo",
-  "status.report": "Reportar",
+  "status.report": "Reportar @{name}",
   "status.sensitive_warning": "Contenido sensible",
   "status.share": "Compartir",
   "status.show_less_all": "Mostrar menos para todo",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
   "tabs_bar.home": "Inicio",
   "tabs_bar.notifications": "Notificaciones",
+  "terms_of_service.effective_as_of": "En vigor a partir del {date}",
   "terms_of_service.title": "Condiciones del servicio",
+  "terms_of_service.upcoming_changes_on": "Próximos cambios el {date}",
   "time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}",
   "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
   "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",
@@ -880,21 +889,21 @@
   "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}",
   "trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} personas}} en los últimos {days, plural, one {días} other {{days} días}}",
   "trends.trending_now": "Tendencia ahora",
-  "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
+  "ui.beforeunload": "Tu borrador se perderá si abandonas Mastodon.",
   "units.short.billion": "{count} MM",
   "units.short.million": "{count} M",
   "units.short.thousand": "{count} K",
   "upload_area.title": "Arrastra y suelta para subir",
   "upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Límite de subida de archivos excedido.",
-  "upload_error.poll": "Subida de archivos no permitida con encuestas.",
+  "upload_error.poll": "No se permite subir archivos con las encuestas.",
   "upload_form.drag_and_drop.instructions": "Para recoger un archivo adjunto, pulsa la barra espaciadora o la tecla Intro. Mientras arrastras, usa las teclas de flecha para mover el archivo adjunto en cualquier dirección. Vuelve a pulsar la barra espaciadora o la tecla Intro para soltar el archivo adjunto en su nueva posición, o pulsa la tecla Escape para cancelar.",
   "upload_form.drag_and_drop.on_drag_cancel": "Arrastre cancelado. El archivo adjunto {item} fue eliminado.",
   "upload_form.drag_and_drop.on_drag_end": "El archivo adjunto {item} fue eliminado.",
   "upload_form.drag_and_drop.on_drag_over": "El archivo adjunto {item} fue movido.",
   "upload_form.drag_and_drop.on_drag_start": "Recogidos los archivos adjuntos {item}.",
   "upload_form.edit": "Editar",
-  "upload_progress.label": "Subiendo…",
+  "upload_progress.label": "Subiendo...",
   "upload_progress.processing": "Procesando…",
   "username.taken": "Ese nombre de usuario está ocupado. Prueba con otro",
   "video.close": "Cerrar video",
@@ -903,8 +912,12 @@
   "video.expand": "Expandir vídeo",
   "video.fullscreen": "Pantalla completa",
   "video.hide": "Ocultar vídeo",
-  "video.mute": "Silenciar sonido",
+  "video.mute": "Silenciar",
   "video.pause": "Pausar",
   "video.play": "Reproducir",
-  "video.unmute": "Dejar de silenciar sonido"
+  "video.skip_backward": "Saltar atrás",
+  "video.skip_forward": "Saltar adelante",
+  "video.unmute": "Dejar de silenciar",
+  "video.volume_down": "Bajar el volumen",
+  "video.volume_up": "Subir el volumen"
 }
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index ddd08c44b3..4f9d91ce11 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Editar perfil",
   "account.enable_notifications": "Notificarme cuando @{name} publique algo",
   "account.endorse": "Destacar en el perfil",
+  "account.featured": "Destacado",
+  "account.featured.hashtags": "Etiquetas",
+  "account.featured.posts": "Publicaciones",
   "account.featured_tags.last_status_at": "Última publicación el {date}",
   "account.featured_tags.last_status_never": "Sin publicaciones",
-  "account.featured_tags.title": "Etiquetas destacadas de {name}",
   "account.follow": "Seguir",
   "account.follow_back": "Seguir también",
   "account.followers": "Seguidores",
@@ -56,22 +58,23 @@
   "account.no_bio": "Sin biografía.",
   "account.open_original_page": "Abrir página original",
   "account.posts": "Publicaciones",
-  "account.posts_with_replies": "Pub. y respuestas",
+  "account.posts_with_replies": "Publicaciones y respuestas",
   "account.report": "Reportar a @{name}",
-  "account.requested": "Esperando aprobación. Clica para cancelar la solicitud de seguimiento",
+  "account.requested": "Esperando aprobación. Haz clic para cancelar la solicitud de seguimiento",
   "account.requested_follow": "{name} ha solicitado seguirte",
   "account.share": "Compartir el perfil de @{name}",
   "account.show_reblogs": "Mostrar impulsos de @{name}",
   "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
   "account.unblock": "Desbloquear a @{name}",
   "account.unblock_domain": "Desbloquear dominio {domain}",
+  "account.unblock_domain_short": "Desbloquear",
   "account.unblock_short": "Desbloquear",
   "account.unendorse": "No mostrar en el perfil",
   "account.unfollow": "Dejar de seguir",
   "account.unmute": "Dejar de silenciar a @{name}",
   "account.unmute_notifications_short": "Dejar de silenciar notificaciones",
   "account.unmute_short": "Dejar de silenciar",
-  "account_note.placeholder": "Clic para añadir nota",
+  "account_note.placeholder": "Haz clic para añadir nota",
   "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después del registro",
   "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después del registro",
   "admin.dashboard.retention.average": "Media",
@@ -81,7 +84,7 @@
   "admin.impact_report.instance_followers": "Seguidores que nuestros usuarios perderían",
   "admin.impact_report.instance_follows": "Seguidores que perderían sus usuarios",
   "admin.impact_report.title": "Resumen de impacto",
-  "alert.rate_limited.message": "Por favor, vuelve a intentarlo después de la(s) {retry_time, time, medium}.",
+  "alert.rate_limited.message": "Por favor, vuelve a intentarlo después de {retry_time, time, medium}.",
   "alert.rate_limited.title": "Tráfico limitado",
   "alert.unexpected.message": "Hubo un error inesperado.",
   "alert.unexpected.title": "¡Ups!",
@@ -93,7 +96,7 @@
   "alt_text_modal.describe_for_people_with_hearing_impairments": "Descríbelo para personas con deficiencias auditivas…",
   "alt_text_modal.describe_for_people_with_visual_impairments": "Descríbelo para personas con discapacidad visual…",
   "alt_text_modal.done": "Hecho",
-  "announcement.announcement": "Anuncio",
+  "announcement.announcement": "Comunicación",
   "annual_report.summary.archetype.booster": "El cazador de tendencias",
   "annual_report.summary.archetype.lurker": "El acechador",
   "annual_report.summary.archetype.oracle": "El oráculo",
@@ -243,7 +246,7 @@
   "copypaste.copied": "Copiado",
   "copypaste.copy_to_clipboard": "Copiar al portapapeles",
   "directory.federated": "Desde el fediverso conocido",
-  "directory.local": "Solo de {domain}",
+  "directory.local": "Solo desde {domain}",
   "directory.new_arrivals": "Recién llegados",
   "directory.recently_active": "Recientemente activo",
   "disabled_account_banner.account_settings": "Ajustes de la cuenta",
@@ -285,7 +288,7 @@
   "emoji_button.food": "Comida y bebida",
   "emoji_button.label": "Insertar emoji",
   "emoji_button.nature": "Naturaleza",
-  "emoji_button.not_found": "No se encontró ningún emoji coincidente",
+  "emoji_button.not_found": "No se encontró ningún emoji que coincida",
   "emoji_button.objects": "Objetos",
   "emoji_button.people": "Personas",
   "emoji_button.recent": "Usados frecuentemente",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Resultados de búsqueda",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viajes y lugares",
+  "empty_column.account_featured": "Esta lista está vacía",
   "empty_column.account_hides_collections": "Este usuario ha decidido no mostrar esta información",
   "empty_column.account_suspended": "Cuenta suspendida",
   "empty_column.account_timeline": "¡No hay publicaciones aquí!",
@@ -316,7 +320,7 @@
   "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo",
   "error.unexpected_crash.explanation": "Debido a un error en nuestro código o a un problema de compatibilidad con el navegador, esta página no se ha podido mostrar correctamente.",
   "error.unexpected_crash.explanation_addons": "No se pudo mostrar correctamente esta página. Este error probablemente fue causado por un complemento del navegador web o por herramientas de traducción automática.",
-  "error.unexpected_crash.next_steps": "Intenta actualizar la página. Si eso no ayuda, es posible que puedas usar Mastodon a través de otro navegador o aplicación nativa.",
+  "error.unexpected_crash.next_steps": "Intenta actualizar la página. Si eso no ayuda, quizás puedas usar Mastodon desde otro navegador o aplicación nativa.",
   "error.unexpected_crash.next_steps_addons": "Intenta deshabilitarlos y recarga la página. Si eso no ayuda, podrías usar Mastodon a través de un navegador web diferente o aplicación nativa.",
   "errors.unexpected_crash.copy_stacktrace": "Copiar el seguimiento de pila en el portapapeles",
   "errors.unexpected_crash.report_issue": "Informar de un problema/error",
@@ -330,7 +334,7 @@
   "filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado, tendrás que cambiar la fecha de caducidad para que se aplique.",
   "filter_modal.added.expired_title": "¡Filtro caducado!",
   "filter_modal.added.review_and_configure": "Para revisar y configurar esta categoría de filtros, vaya a {settings_link}.",
-  "filter_modal.added.review_and_configure_title": "Ajustes de filtro",
+  "filter_modal.added.review_and_configure_title": "Ajustes de filtros",
   "filter_modal.added.settings_link": "página de ajustes",
   "filter_modal.added.short_explanation": "Esta publicación ha sido añadida a la siguiente categoría de filtros: {title}.",
   "filter_modal.added.title": "¡Filtro añadido!",
@@ -352,7 +356,7 @@
   "follow_requests.unlocked_explanation": "A pesar de que tu cuenta no es privada, el personal de {domain} ha pensado que quizás deberías revisar manualmente las solicitudes de seguimiento de estas cuentas.",
   "follow_suggestions.curated_suggestion": "Recomendaciones del equipo",
   "follow_suggestions.dismiss": "No mostrar de nuevo",
-  "follow_suggestions.featured_longer": "Escogidos por el equipo de {domain}",
+  "follow_suggestions.featured_longer": "Sugerencias del equipo de {domain}",
   "follow_suggestions.friends_of_friends_longer": "Populares entre las personas a las que sigues",
   "follow_suggestions.hints.featured": "Este perfil ha sido elegido a mano por el equipo de {domain}.",
   "follow_suggestions.hints.friends_of_friends": "Este perfil es popular entre las personas que sigues.",
@@ -377,19 +381,22 @@
   "generic.saved": "Guardado",
   "getting_started.heading": "Primeros pasos",
   "hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
+  "hashtag.browse": "Explorar publicaciones en #{hashtag}",
+  "hashtag.browse_from_account": "Explorar publicaciones desde @{name} en #{hashtag}",
   "hashtag.column_header.tag_mode.all": "y {additional}",
   "hashtag.column_header.tag_mode.any": "o {additional}",
   "hashtag.column_header.tag_mode.none": "sin {additional}",
-  "hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias",
+  "hashtag.column_settings.select.no_options_message": "No se han encontrado sugerencias",
   "hashtag.column_settings.select.placeholder": "Introduce etiquetas…",
-  "hashtag.column_settings.tag_mode.all": "Todos estos",
-  "hashtag.column_settings.tag_mode.any": "Cualquiera de estos",
-  "hashtag.column_settings.tag_mode.none": "Ninguno de estos",
+  "hashtag.column_settings.tag_mode.all": "Todas estas",
+  "hashtag.column_settings.tag_mode.any": "Cualquiera de estas",
+  "hashtag.column_settings.tag_mode.none": "Ninguna de estas",
   "hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionales en esta columna",
   "hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
   "hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}} hoy",
   "hashtag.follow": "Seguir etiqueta",
+  "hashtag.mute": "Silenciar #{hashtag}",
   "hashtag.unfollow": "Dejar de seguir etiqueta",
   "hashtags.and_other": "…y {count, plural, other {# más}}",
   "hints.profiles.followers_may_be_missing": "Puede que no se muestren todos los seguidores de este perfil.",
@@ -402,12 +409,12 @@
   "hints.threads.see_more": "Ver más respuestas en {domain}",
   "home.column_settings.show_reblogs": "Mostrar impulsos",
   "home.column_settings.show_replies": "Mostrar respuestas",
-  "home.hide_announcements": "Ocultar anuncios",
+  "home.hide_announcements": "Ocultar comunicaciones",
   "home.pending_critical_update.body": "Por favor, ¡actualiza tu servidor Mastodon lo antes posible!",
   "home.pending_critical_update.link": "Ver actualizaciones",
   "home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!",
-  "home.show_announcements": "Mostrar anuncios",
-  "ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios que has ignorado sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.",
+  "home.show_announcements": "Mostrar comunicaciones",
+  "ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios de que has ignorado sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.",
   "ignore_notifications_modal.filter_instead": "Filtrar en vez de ignorar",
   "ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o reportar usuarios",
   "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrar ayuda a evitar confusiones potenciales",
@@ -438,14 +445,14 @@
   "intervals.full.days": "{number, plural, one {# día} other {# días}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
-  "keyboard_shortcuts.back": "volver atrás",
-  "keyboard_shortcuts.blocked": "abrir una lista de usuarios bloqueados",
+  "keyboard_shortcuts.back": "Navegar hacia atrás",
+  "keyboard_shortcuts.blocked": "Abrir lista de usuarios bloqueados",
   "keyboard_shortcuts.boost": "Impulsar",
   "keyboard_shortcuts.column": "Enfocar columna",
-  "keyboard_shortcuts.compose": "enfocar el área de texto de redacción",
+  "keyboard_shortcuts.compose": "Focalizar el área de texto de redacción",
   "keyboard_shortcuts.description": "Descripción",
   "keyboard_shortcuts.direct": "para abrir la columna de menciones privadas",
-  "keyboard_shortcuts.down": "mover hacia abajo en la lista",
+  "keyboard_shortcuts.down": "Moverse hacia abajo en la lista",
   "keyboard_shortcuts.enter": "Abrir publicación",
   "keyboard_shortcuts.favourite": "Marcar como favorita la publicación",
   "keyboard_shortcuts.favourites": "Abrir lista de favoritos",
@@ -455,24 +462,24 @@
   "keyboard_shortcuts.hotkey": "Tecla rápida",
   "keyboard_shortcuts.legend": "Mostrar esta leyenda",
   "keyboard_shortcuts.local": "Abrir cronología local",
-  "keyboard_shortcuts.mention": "mencionar al autor",
-  "keyboard_shortcuts.muted": "abrir la lista de usuarios silenciados",
-  "keyboard_shortcuts.my_profile": "abrir tu perfil",
-  "keyboard_shortcuts.notifications": "abrir la columna de notificaciones",
-  "keyboard_shortcuts.open_media": "para abrir archivos multimedia",
+  "keyboard_shortcuts.mention": "Mencionar autor",
+  "keyboard_shortcuts.muted": "Abrir lista de usuarios silenciados",
+  "keyboard_shortcuts.my_profile": "Abrir tu perfil",
+  "keyboard_shortcuts.notifications": "Abrir columna de notificaciones",
+  "keyboard_shortcuts.open_media": "Abrir multimedia",
   "keyboard_shortcuts.pinned": "Abrir la lista de publicaciones destacadas",
-  "keyboard_shortcuts.profile": "abrir el perfil del autor",
-  "keyboard_shortcuts.reply": "para responder",
-  "keyboard_shortcuts.requests": "abrir la lista de peticiones de seguidores",
-  "keyboard_shortcuts.search": "para poner el foco en la búsqueda",
-  "keyboard_shortcuts.spoilers": "para mostrar/ocultar el campo CW",
-  "keyboard_shortcuts.start": "abrir la columna \"comenzar\"",
-  "keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)",
-  "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios",
-  "keyboard_shortcuts.toot": "Comienza una nueva publicación",
+  "keyboard_shortcuts.profile": "Abrir perfil del autor",
+  "keyboard_shortcuts.reply": "Responder a una publicación",
+  "keyboard_shortcuts.requests": "Abrir lista de solicitudes de seguimiento",
+  "keyboard_shortcuts.search": "Focalizar barra de búsqueda",
+  "keyboard_shortcuts.spoilers": "Mostrar/ocultar el campo de CW",
+  "keyboard_shortcuts.start": "Abrir la columna \"comenzar\"",
+  "keyboard_shortcuts.toggle_hidden": "Mostrar/ocultar texto tras aviso de contenido (CW)",
+  "keyboard_shortcuts.toggle_sensitivity": "Mostrar/ocultar multimedia",
+  "keyboard_shortcuts.toot": "Comenzar una nueva publicación",
   "keyboard_shortcuts.translate": "para traducir una publicación",
-  "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
-  "keyboard_shortcuts.up": "para ir hacia arriba en la lista",
+  "keyboard_shortcuts.unfocus": "Quitar el foco de la caja de redacción/búsqueda",
+  "keyboard_shortcuts.up": "Moverse hacia arriba en la lista",
   "lightbox.close": "Cerrar",
   "lightbox.next": "Siguiente",
   "lightbox.previous": "Anterior",
@@ -644,7 +651,7 @@
   "notifications.grant_permission": "Conceder permiso.",
   "notifications.group": "{count} notificaciones",
   "notifications.mark_as_read": "Marcar todas las notificaciones como leídas",
-  "notifications.permission_denied": "No se pueden habilitar las notificaciones de escritorio ya que se denegó el permiso.",
+  "notifications.permission_denied": "Las notificaciones de escritorio no están disponibles porque se denegó el permiso del navegador previamente",
   "notifications.permission_denied_alert": "No se pueden habilitar las notificaciones de escritorio, ya que el permiso del navegador fue denegado anteriormente",
   "notifications.permission_required": "Las notificaciones de escritorio no están disponibles porque no se ha concedido el permiso requerido.",
   "notifications.policy.accept": "Aceptar",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Eliminar encuesta",
   "privacy.change": "Ajustar privacidad",
   "privacy.direct.long": "Visible únicamente por los mencionados en la publicación",
-  "privacy.direct.short": "Personas específicas",
+  "privacy.direct.short": "Mención privada",
   "privacy.private.long": "Visible únicamente por tus seguidores",
   "privacy.private.short": "Seguidores",
   "privacy.public.long": "Visible por todo el mundo, dentro y fuera de Mastodon",
@@ -835,7 +842,7 @@
   "status.media.open": "Pulsa para abrir",
   "status.media.show": "Pulsa para mostrar",
   "status.media_hidden": "Contenido multimedia oculto",
-  "status.mention": "Mencionar",
+  "status.mention": "Mencionar a @{name}",
   "status.more": "Más",
   "status.mute": "Silenciar @{name}",
   "status.mute_conversation": "Silenciar conversación",
@@ -855,7 +862,7 @@
   "status.replied_to": "Respondió a {name}",
   "status.reply": "Responder",
   "status.replyAll": "Responder al hilo",
-  "status.report": "Reportar",
+  "status.report": "Reportar a @{name}",
   "status.sensitive_warning": "Contenido sensible",
   "status.share": "Compartir",
   "status.show_less_all": "Mostrar menos para todo",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
   "tabs_bar.home": "Inicio",
   "tabs_bar.notifications": "Notificaciones",
+  "terms_of_service.effective_as_of": "En vigor a partir del {date}",
   "terms_of_service.title": "Términos del servicio",
+  "terms_of_service.upcoming_changes_on": "Próximos cambios el {date}",
   "time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}",
   "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
   "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",
@@ -885,7 +894,7 @@
   "units.short.million": "{count} M",
   "units.short.thousand": "{count} K",
   "upload_area.title": "Arrastra y suelta para subir",
-  "upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
+  "upload_button.label": "Añadir imágenes, un fichero de vídeo o de audio",
   "upload_error.limit": "Límite de subida de archivos excedido.",
   "upload_error.poll": "No se permite la subida de archivos con encuestas.",
   "upload_form.drag_and_drop.instructions": "Para recoger un archivo multimedia, pulsa la barra espaciadora o la tecla Enter. Mientras arrastras, utiliza las teclas de flecha para mover el archivo multimedia en cualquier dirección. Vuelve a pulsar la barra espaciadora o la tecla Enter para soltar el archivo multimedia en su nueva posición, o pulsa Escape para cancelar.",
@@ -894,7 +903,7 @@
   "upload_form.drag_and_drop.on_drag_over": "El archivo adjunto {item} se ha movido.",
   "upload_form.drag_and_drop.on_drag_start": "Se ha recogido el archivo adjunto {item}.",
   "upload_form.edit": "Editar",
-  "upload_progress.label": "Subiendo…",
+  "upload_progress.label": "Subiendo...",
   "upload_progress.processing": "Procesando…",
   "username.taken": "Ese nombre de usuario ya está en uso. Prueba con otro",
   "video.close": "Cerrar video",
@@ -903,8 +912,12 @@
   "video.expand": "Expandir vídeo",
   "video.fullscreen": "Pantalla completa",
   "video.hide": "Ocultar vídeo",
-  "video.mute": "Silenciar sonido",
+  "video.mute": "Silenciar",
   "video.pause": "Pausar",
   "video.play": "Reproducir",
-  "video.unmute": "Desilenciar sonido"
+  "video.skip_backward": "Saltar atrás",
+  "video.skip_forward": "Adelantar",
+  "video.unmute": "Dejar de silenciar",
+  "video.volume_down": "Bajar volumen",
+  "video.volume_up": "Subir volumen"
 }
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
index bd86e984aa..3e0610126e 100644
--- a/app/javascript/mastodon/locales/et.json
+++ b/app/javascript/mastodon/locales/et.json
@@ -29,7 +29,6 @@
   "account.endorse": "Too profiilil esile",
   "account.featured_tags.last_status_at": "Viimane postitus {date}",
   "account.featured_tags.last_status_never": "Postitusi pole",
-  "account.featured_tags.title": "{name} esiletõstetud sildid",
   "account.follow": "Jälgi",
   "account.follow_back": "Jälgi vastu",
   "account.followers": "Jälgijad",
@@ -218,6 +217,10 @@
   "confirmations.logout.confirm": "Välju",
   "confirmations.logout.message": "Kas oled kindel, et soovid välja logida?",
   "confirmations.logout.title": "Logida välja?",
+  "confirmations.missing_alt_text.confirm": "Lisa alt-tekst",
+  "confirmations.missing_alt_text.message": "Sinu postituses on ilma alt-tekstita meediat. Kirjelduse lisamine aitab su sisu muuta ligipääsetavaks rohkematele inimestele.",
+  "confirmations.missing_alt_text.secondary": "Postita siiski",
+  "confirmations.missing_alt_text.title": "Lisada alt-tekst?",
   "confirmations.mute.confirm": "Vaigista",
   "confirmations.redraft.confirm": "Kustuta & taasalusta",
   "confirmations.redraft.message": "Kindel, et soovid postituse kustutada ja võtta uue aluseks? Lemmikuks märkimised ja jagamised lähevad kaotsi ning vastused jäävad ilma algse postituseta.",
@@ -553,8 +556,12 @@
   "notification.admin.report_statuses_other": "{name} raporteeris kohast {target}",
   "notification.admin.sign_up": "{name} registreerus",
   "notification.admin.sign_up.name_and_others": "{name} ja {count, plural, one {# veel} other {# teist}} liitus",
+  "notification.annual_report.message": "Sinu {year} #Wrapstodon ootab! Avalda oma aasta tipphetked ja meeldejäävad hetked Mastodonis!",
+  "notification.annual_report.view": "Vaata #Wrapstodon",
   "notification.favourite": "{name} märkis su postituse lemmikuks",
   "notification.favourite.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# teist}}</a> märkis su postituse lemmikuks",
+  "notification.favourite_pm": "{name} märkis sinu privaatse mainimise lemmikuks",
+  "notification.favourite_pm.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# veel}}</a> märkisid su privaatse mainimise lemmikuks",
   "notification.follow": "{name} alustas su jälgimist",
   "notification.follow.name_and_others": "{name} ja veel {count, plural, one {# kasutaja} other {# kasutajat}} hakkas sind jälgima",
   "notification.follow_request": "{name} soovib sind jälgida",
@@ -689,7 +696,7 @@
   "poll_button.remove_poll": "Eemalda küsitlus",
   "privacy.change": "Muuda postituse nähtavust",
   "privacy.direct.long": "Kõik postituses mainitud",
-  "privacy.direct.short": "Määratud kasutajad",
+  "privacy.direct.short": "Privaatne mainimine",
   "privacy.private.long": "Ainult jälgijad",
   "privacy.private.short": "Jälgijad",
   "privacy.public.long": "Nii kasutajad kui mittekasutajad",
@@ -889,8 +896,6 @@
   "video.expand": "Suurenda video",
   "video.fullscreen": "Täisekraan",
   "video.hide": "Peida video",
-  "video.mute": "Vaigista heli",
   "video.pause": "Paus",
-  "video.play": "Mängi",
-  "video.unmute": "Taasta heli"
+  "video.play": "Mängi"
 }
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 1341bb17f3..0c73c9f540 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -29,7 +29,6 @@
   "account.endorse": "Nabarmendu profilean",
   "account.featured_tags.last_status_at": "Azken bidalketa {date} datan",
   "account.featured_tags.last_status_never": "Bidalketarik ez",
-  "account.featured_tags.title": "{name} erabiltzailearen nabarmendutako traolak",
   "account.follow": "Jarraitu",
   "account.follow_back": "Jarraitu bueltan",
   "account.followers": "Jarraitzaileak",
@@ -208,6 +207,9 @@
   "confirmations.logout.confirm": "Amaitu saioa",
   "confirmations.logout.message": "Ziur saioa amaitu nahi duzula?",
   "confirmations.logout.title": "Itxi saioa?",
+  "confirmations.missing_alt_text.confirm": "Gehitu testu alternatiboa",
+  "confirmations.missing_alt_text.secondary": "Bidali edonola ere",
+  "confirmations.missing_alt_text.title": "Testu alternatiboa gehitu?",
   "confirmations.mute.confirm": "Mututu",
   "confirmations.redraft.confirm": "Ezabatu eta berridatzi",
   "confirmations.redraft.message": "Ziur argitalpen hau ezabatu eta zirriborroa berriro egitea nahi duzula? Gogokoak eta bultzadak galduko dira, eta jatorrizko argitalpenaren erantzunak zurtz geratuko dira.",
@@ -641,7 +643,6 @@
   "poll_button.remove_poll": "Kendu inkesta",
   "privacy.change": "Aldatu bidalketaren pribatutasuna",
   "privacy.direct.long": "Argitalpen honetan aipatutako denak",
-  "privacy.direct.short": "Jende jakina",
   "privacy.private.long": "Soilik jarraitzaileak",
   "privacy.private.short": "Jarraitzaileak",
   "privacy.public.long": "Mastodonen dagoen edo ez dagoen edonor",
@@ -838,8 +839,6 @@
   "video.expand": "Hedatu bideoa",
   "video.fullscreen": "Pantaila osoa",
   "video.hide": "Ezkutatu bideoa",
-  "video.mute": "Mututu soinua",
   "video.pause": "Pausatu",
-  "video.play": "Jo",
-  "video.unmute": "Desmututu soinua"
+  "video.play": "Jo"
 }
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 625acc01a9..3e31eb8a15 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -29,7 +29,6 @@
   "account.endorse": "معرّفی در نمایه",
   "account.featured_tags.last_status_at": "آخرین فرسته در {date}",
   "account.featured_tags.last_status_never": "بدون فرسته",
-  "account.featured_tags.title": "برچسب‌های برگزیدهٔ {name}",
   "account.follow": "پی‌گرفتن",
   "account.follow_back": "دنبال کردن متقابل",
   "account.followers": "پی‌گیرندگان",
@@ -65,6 +64,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",
   "account.unblock": "رفع مسدودیت ‎@{name}",
   "account.unblock_domain": "رفع مسدودیت دامنهٔ {domain}",
+  "account.unblock_domain_short": "آنبلاک",
   "account.unblock_short": "رفع مسدودیت",
   "account.unendorse": "معرّفی نکردن در نمایه",
   "account.unfollow": "پی‌نگرفتن",
@@ -218,6 +218,10 @@
   "confirmations.logout.confirm": "خروج از حساب",
   "confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟",
   "confirmations.logout.title": "خروج؟",
+  "confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
+  "confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.",
+  "confirmations.missing_alt_text.secondary": "به هر حال پست کن",
+  "confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
   "confirmations.mute.confirm": "خموش",
   "confirmations.redraft.confirm": "حذف و بازنویسی",
   "confirmations.redraft.message": "مطمئنید که می‌خواهید این فرسته را حذف کنید و از نو بنویسید؟ با این کار تقویت‌ها و پسندهایش از دست رفته و پاسخ‌ها به آن بی‌مرجع می‌شود.",
@@ -693,7 +697,7 @@
   "poll_button.remove_poll": "برداشتن نظرسنجی",
   "privacy.change": "تغییر محرمانگی فرسته",
   "privacy.direct.long": "هرکسی که در فرسته نام برده شده",
-  "privacy.direct.short": "افراد مشخّص",
+  "privacy.direct.short": "ذکر خصوصی",
   "privacy.private.long": "تنها پی‌گیرندگانتان",
   "privacy.private.short": "پی‌گیرندگان",
   "privacy.public.long": "هرکسی در و بیرون از ماستودون",
@@ -868,7 +872,9 @@
   "subscribed_languages.target": "تغییر زبان‌های مشترک شده برای {target}",
   "tabs_bar.home": "خانه",
   "tabs_bar.notifications": "آگاهی‌ها",
+  "terms_of_service.effective_as_of": "اعمال شده از {date}",
   "terms_of_service.title": "شرایط خدمات",
+  "terms_of_service.upcoming_changes_on": "تغییرات پیش رو در {date}",
   "time_remaining.days": "{number, plural, one {# روز} other {# روز}} باقی مانده",
   "time_remaining.hours": "{number, plural, one {# ساعت} other {# ساعت}} باقی مانده",
   "time_remaining.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}} باقی مانده",
@@ -899,8 +905,6 @@
   "video.expand": "گسترش ویدیو",
   "video.fullscreen": "تمام‌صفحه",
   "video.hide": "نهفتن ویدیو",
-  "video.mute": "خموشی صدا",
   "video.pause": "مکث",
-  "video.play": "پخش",
-  "video.unmute": "لغو خموشی صدا"
+  "video.play": "پخش"
 }
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index fab5775e90..cc42780f94 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -27,16 +27,17 @@
   "account.edit_profile": "Muokkaa profiilia",
   "account.enable_notifications": "Ilmoita minulle, kun @{name} julkaisee",
   "account.endorse": "Suosittele profiilissasi",
+  "account.featured.hashtags": "Aihetunnisteet",
+  "account.featured.posts": "Julkaisut",
   "account.featured_tags.last_status_at": "Viimeisin julkaisu {date}",
   "account.featured_tags.last_status_never": "Ei julkaisuja",
-  "account.featured_tags.title": "Käyttäjän {name} suosittelemat aihetunnisteet",
   "account.follow": "Seuraa",
   "account.follow_back": "Seuraa takaisin",
   "account.followers": "Seuraajat",
   "account.followers.empty": "Kukaan ei seuraa tätä käyttäjää vielä.",
   "account.followers_counter": "{count, plural, one {{counter} seuraaja} other {{counter} seuraajaa}}",
-  "account.following": "Seuratut",
-  "account.following_counter": "{count, plural, one {{counter} seurattu} other {{counter} seurattua}}",
+  "account.following": "Seurattavat",
+  "account.following_counter": "{count, plural, one {{counter} seurattava} other {{counter} seurattavaa}}",
   "account.follows.empty": "Tämä käyttäjä ei vielä seuraa ketään.",
   "account.go_to_profile": "Siirry profiiliin",
   "account.hide_reblogs": "Piilota käyttäjän @{name} tehostukset",
@@ -65,6 +66,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} julkaisu} other {{counter} julkaisua}}",
   "account.unblock": "Kumoa käyttäjän @{name} esto",
   "account.unblock_domain": "Kumoa verkkotunnuksen {domain} esto",
+  "account.unblock_domain_short": "Kumoa esto",
   "account.unblock_short": "Kumoa esto",
   "account.unendorse": "Kumoa suosittelu profiilissasi",
   "account.unfollow": "Älä seuraa",
@@ -239,7 +241,7 @@
   "conversation.mark_as_read": "Merkitse luetuksi",
   "conversation.open": "Näytä keskustelu",
   "conversation.with": "{names} kanssa",
-  "copy_icon_button.copied": "Sisältö kopioitiin leikepöydälle",
+  "copy_icon_button.copied": "Kopioitu leikepöydälle",
   "copypaste.copied": "Kopioitu",
   "copypaste.copy_to_clipboard": "Kopioi leikepöydälle",
   "directory.federated": "Tunnetusta fediversumista",
@@ -260,8 +262,8 @@
   "domain_block_modal.they_cant_follow": "Kukaan tältä palvelimelta ei voi seurata sinua.",
   "domain_block_modal.they_wont_know": "Hän ei saa tietää tulleensa estetyksi.",
   "domain_block_modal.title": "Estetäänkö verkkotunnus?",
-  "domain_block_modal.you_will_lose_num_followers": "Menetät {followersCount, plural, one {{followersCountDisplay} seuraajasi} other {{followersCountDisplay} seuraajaasi}} ja {followingCount, plural, one {{followingCountDisplay} seurattusi} other {{followingCountDisplay} seurattuasi}}.",
-  "domain_block_modal.you_will_lose_relationships": "Menetät kaikki tämän palvelimen seuraajasi ja seurattusi.",
+  "domain_block_modal.you_will_lose_num_followers": "Menetät {followersCount, plural, one {{followersCountDisplay} seuraajasi} other {{followersCountDisplay} seuraajaasi}} ja {followingCount, plural, one {{followingCountDisplay} seurattavasi} other {{followingCountDisplay} seurattavaasi}}.",
+  "domain_block_modal.you_will_lose_relationships": "Menetät kaikki tämän palvelimen seuraajasi ja seurattavasi.",
   "domain_block_modal.you_wont_see_posts": "Et enää näe julkaisuja etkä ilmoituksia tämän palvelimen käyttäjiltä.",
   "domain_pill.activitypub_lets_connect": "Sen avulla voit muodostaa yhteyden ja olla vuorovaikutuksessa ihmisten kanssa, ei vain Mastodonissa vaan myös muissa sosiaalisissa sovelluksissa.",
   "domain_pill.activitypub_like_language": "ActivityPub on kuin kieli, jota Mastodon puhuu muiden sosiaalisten verkostojen kanssa.",
@@ -293,6 +295,7 @@
   "emoji_button.search_results": "Hakutulokset",
   "emoji_button.symbols": "Symbolit",
   "emoji_button.travel": "Matkailu ja paikat",
+  "empty_column.account_featured": "Tämä lista on tyhjä",
   "empty_column.account_hides_collections": "Käyttäjä on päättänyt pitää nämä tiedot yksityisinä",
   "empty_column.account_suspended": "Tili jäädytetty",
   "empty_column.account_timeline": "Ei viestejä täällä.",
@@ -365,7 +368,7 @@
   "follow_suggestions.similar_to_recently_followed_longer": "Samankaltainen kuin äskettäin seuraamasi profiilit",
   "follow_suggestions.view_all": "Näytä kaikki",
   "follow_suggestions.who_to_follow": "Ehdotuksia seurattavaksi",
-  "followed_tags": "Seuratut aihetunnisteet",
+  "followed_tags": "Seurattavat aihetunnisteet",
   "footer.about": "Tietoja",
   "footer.directory": "Profiilihakemisto",
   "footer.get_app": "Hanki sovellus",
@@ -377,6 +380,8 @@
   "generic.saved": "Tallennettu",
   "getting_started.heading": "Näin pääset alkuun",
   "hashtag.admin_moderation": "Avaa tunnisteen #{name} moderointinäkymä",
+  "hashtag.browse": "Selaa julkaisuja tunnisteella #{hashtag}",
+  "hashtag.browse_from_account": "Selaa julkaisuja käyttäjältä @{name} tunnisteella #{hashtag}",
   "hashtag.column_header.tag_mode.all": "ja {additional}",
   "hashtag.column_header.tag_mode.any": "tai {additional}",
   "hashtag.column_header.tag_mode.none": "ilman {additional}",
@@ -390,13 +395,14 @@
   "hashtag.counter_by_uses": "{count, plural, one{{counter} julkaisu} other {{counter} julkaisua}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} julkaisu} other {{counter} julkaisua}} tänään",
   "hashtag.follow": "Seuraa aihetunnistetta",
+  "hashtag.mute": "Mykistä #{hashtag}",
   "hashtag.unfollow": "Lopeta aihetunnisteen seuraaminen",
   "hashtags.and_other": "…ja {count, plural, other {# lisää}}",
   "hints.profiles.followers_may_be_missing": "Tämän profiilin seuraajia saattaa puuttua.",
-  "hints.profiles.follows_may_be_missing": "Tämän profiilin seurattuja saattaa puuttua.",
+  "hints.profiles.follows_may_be_missing": "Tämän profiilin seurattavia saattaa puuttua.",
   "hints.profiles.posts_may_be_missing": "Tämän profiilin julkaisuja saattaa puuttua.",
   "hints.profiles.see_more_followers": "Näytä lisää seuraajia palvelimella {domain}",
-  "hints.profiles.see_more_follows": "Näytä lisää seurattuja palvelimella {domain}",
+  "hints.profiles.see_more_follows": "Näytä lisää seurattavia palvelimella {domain}",
   "hints.profiles.see_more_posts": "Näytä lisää julkaisuja palvelimella {domain}",
   "hints.threads.replies_may_be_missing": "Muiden palvelinten vastauksia saattaa puuttua.",
   "hints.threads.see_more": "Näytä lisää vastauksia palvelimella {domain}",
@@ -503,7 +509,7 @@
   "lists.no_members_yet": "Ei vielä jäseniä.",
   "lists.no_results_found": "Tuloksia ei löytynyt.",
   "lists.remove_member": "Poista",
-  "lists.replies_policy.followed": "Jokaiselle seuratulle käyttäjälle",
+  "lists.replies_policy.followed": "Jokaiselle seurattavalle käyttäjälle",
   "lists.replies_policy.list": "Listan jäsenille",
   "lists.replies_policy.none": "Ei kellekään",
   "lists.save": "Tallenna",
@@ -536,8 +542,8 @@
   "navigation_bar.favourites": "Suosikit",
   "navigation_bar.filters": "Mykistetyt sanat",
   "navigation_bar.follow_requests": "Seurantapyynnöt",
-  "navigation_bar.followed_tags": "Seuratut aihetunnisteet",
-  "navigation_bar.follows_and_followers": "Seuratut ja seuraajat",
+  "navigation_bar.followed_tags": "Seurattavat aihetunnisteet",
+  "navigation_bar.follows_and_followers": "Seurattavat ja seuraajat",
   "navigation_bar.lists": "Listat",
   "navigation_bar.logout": "Kirjaudu ulos",
   "navigation_bar.moderation": "Moderointi",
@@ -588,9 +594,9 @@
   "notification.reblog.name_and_others_with_link": "{name} ja <a>{count, plural, one {# muu} other {# muuta}}</a> tehostivat julkaisuasi",
   "notification.relationships_severance_event": "Menetettiin yhteydet palvelimeen {name}",
   "notification.relationships_severance_event.account_suspension": "Palvelimen {from} ylläpitäjä on jäädyttänyt palvelimen {target} vuorovaikutuksen. Enää et voi siis vastaanottaa päivityksiä heiltä tai olla yhteyksissä heidän kanssaan.",
-  "notification.relationships_severance_event.domain_block": "Palvelimen {from} ylläpitäjä on estänyt palvelimen {target} vuorovaikutuksen – mukaan lukien {followersCount} seuraajistasi ja {followingCount, plural, one {# seuratuistasi} other {# seuratuistasi}}.",
+  "notification.relationships_severance_event.domain_block": "Palvelimen {from} ylläpitäjä on estänyt palvelimen {target} vuorovaikutuksen – mukaan lukien {followersCount} seuraajistasi ja {followingCount, plural, one {# seurattavistasi} other {# seurattavistasi}}.",
   "notification.relationships_severance_event.learn_more": "Lue lisää",
-  "notification.relationships_severance_event.user_domain_block": "Olet estänyt verkkotunnuksen {target}, mikä poisti {followersCount} seuraajistasi ja {followingCount, plural, one {# seuratuistasi} other {# seuratuistasi}}.",
+  "notification.relationships_severance_event.user_domain_block": "Olet estänyt palvelimen {target}, mikä poisti {followersCount} seuraajistasi ja {followingCount, plural, one {# seurattavistasi} other {# seurattavistasi}}.",
   "notification.status": "{name} julkaisi juuri",
   "notification.update": "{name} muokkasi julkaisua",
   "notification_requests.accept": "Hyväksy",
@@ -625,7 +631,7 @@
   "notifications.column_settings.follow_request": "Uudet seurantapyynnöt:",
   "notifications.column_settings.group": "Ryhmitä",
   "notifications.column_settings.mention": "Maininnat:",
-  "notifications.column_settings.poll": "Äänestyksen tulokset:",
+  "notifications.column_settings.poll": "Äänestystulokset:",
   "notifications.column_settings.push": "Puskuilmoitukset",
   "notifications.column_settings.reblog": "Tehostukset:",
   "notifications.column_settings.show": "Näytä sarakkeessa",
@@ -639,7 +645,7 @@
   "notifications.filter.favourites": "Suosikit",
   "notifications.filter.follows": "Seuraamiset",
   "notifications.filter.mentions": "Maininnat",
-  "notifications.filter.polls": "Äänestyksen tulokset",
+  "notifications.filter.polls": "Äänestystulokset",
   "notifications.filter.statuses": "Päivitykset seuraamiltasi käyttäjiltä",
   "notifications.grant_permission": "Myönnä käyttöoikeus.",
   "notifications.group": "{count} ilmoitusta",
@@ -697,7 +703,7 @@
   "poll_button.remove_poll": "Poista äänestys",
   "privacy.change": "Muuta julkaisun näkyvyyttä",
   "privacy.direct.long": "Kaikki tässä julkaisussa mainitut",
-  "privacy.direct.short": "Tietyt käyttäjät",
+  "privacy.direct.short": "Yksityismaininta",
   "privacy.private.long": "Vain seuraajasi",
   "privacy.private.short": "Seuraajat",
   "privacy.public.long": "Kuka tahansa Mastodonissa ja sen ulkopuolella",
@@ -872,7 +878,9 @@
   "subscribed_languages.target": "Vaihda tilattuja kieliä käyttäjältä {target}",
   "tabs_bar.home": "Koti",
   "tabs_bar.notifications": "Ilmoitukset",
+  "terms_of_service.effective_as_of": "Tulee voimaan {date}",
   "terms_of_service.title": "Käyttöehdot",
+  "terms_of_service.upcoming_changes_on": "Tulevia muutoksia {date}",
   "time_remaining.days": "{number, plural, one {# päivä} other {# päivää}} jäljellä",
   "time_remaining.hours": "{number, plural, one {# tunti} other {# tuntia}} jäljellä",
   "time_remaining.minutes": "{number, plural, one {# minuutti} other {# minuuttia}} jäljellä",
@@ -903,8 +911,12 @@
   "video.expand": "Laajenna video",
   "video.fullscreen": "Koko näyttö",
   "video.hide": "Piilota video",
-  "video.mute": "Mykistä ääni",
+  "video.mute": "Mykistä",
   "video.pause": "Tauko",
   "video.play": "Toista",
-  "video.unmute": "Palauta ääni"
+  "video.skip_backward": "Siirry taaksepäin",
+  "video.skip_forward": "Siirry eteenpäin",
+  "video.unmute": "Poista mykistys",
+  "video.volume_down": "Vähennä äänenvoimakkuutta",
+  "video.volume_up": "Lisää äänenvoimakkuutta"
 }
diff --git a/app/javascript/mastodon/locales/fil.json b/app/javascript/mastodon/locales/fil.json
index 4c33ac01a1..c13d0a8afe 100644
--- a/app/javascript/mastodon/locales/fil.json
+++ b/app/javascript/mastodon/locales/fil.json
@@ -29,7 +29,6 @@
   "account.endorse": "I-tampok sa profile",
   "account.featured_tags.last_status_at": "Huling post noong {date}",
   "account.featured_tags.last_status_never": "Walang mga post",
-  "account.featured_tags.title": "Nakatampok na hashtag ni {name}",
   "account.follow": "Sundan",
   "account.follow_back": "Sundan pabalik",
   "account.followers": "Mga tagasunod",
diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json
index 00499b3c24..b472fd8bd2 100644
--- a/app/javascript/mastodon/locales/fo.json
+++ b/app/javascript/mastodon/locales/fo.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Broyt vanga",
   "account.enable_notifications": "Boða mær frá, tá @{name} skrivar",
   "account.endorse": "Víst á vangamyndini",
+  "account.featured": "Tikin fram",
+  "account.featured.hashtags": "Frámerki",
+  "account.featured.posts": "Postar",
   "account.featured_tags.last_status_at": "Seinasta strongur skrivaður {date}",
   "account.featured_tags.last_status_never": "Einki uppslag",
-  "account.featured_tags.title": "Tvíkrossar hjá {name}",
   "account.follow": "Fylg",
   "account.follow_back": "Fylg aftur",
   "account.followers": "Fylgjarar",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} postur} other {{counter} postar}}",
   "account.unblock": "Banna ikki @{name}",
   "account.unblock_domain": "Banna ikki økisnavnið {domain}",
+  "account.unblock_domain_short": "Banna ikki",
   "account.unblock_short": "Banna ikki",
   "account.unendorse": "Vís ikki á vanga",
   "account.unfollow": "Fylg ikki",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Leitiúrslit",
   "emoji_button.symbols": "Ímyndir",
   "emoji_button.travel": "Ferðing og støð",
+  "empty_column.account_featured": "Hesin listin er tómur",
   "empty_column.account_hides_collections": "Hesin brúkarin hevur valt, at hesar upplýsingarnar ikki skulu vera tøkar",
   "empty_column.account_suspended": "Kontan gjørd óvirkin",
   "empty_column.account_timeline": "Einki uppslag her!",
@@ -377,6 +381,8 @@
   "generic.saved": "Goymt",
   "getting_started.heading": "At byrja",
   "hashtag.admin_moderation": "Lat umsjónarmarkamót upp fyri #{name}",
+  "hashtag.browse": "Blaða gjøgnum postar í #{hashtag}",
+  "hashtag.browse_from_account": "Blaða gjøgnum postar frá @{name} í #{hashtag}",
   "hashtag.column_header.tag_mode.all": "og {additional}",
   "hashtag.column_header.tag_mode.any": "ella {additional}",
   "hashtag.column_header.tag_mode.none": "uttan {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} postur} other {{counter} postar}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} postur} other {{counter} postar}} í dag",
   "hashtag.follow": "Fylg frámerki",
+  "hashtag.mute": "Doyv @#{hashtag}",
   "hashtag.unfollow": "Gevst at fylgja frámerki",
   "hashtags.and_other": "…og {count, plural, other {# afturat}}",
   "hints.profiles.followers_may_be_missing": "Fylgjarar hjá hesum vanganum kunnu mangla.",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Strika atkvøðugreiðslu",
   "privacy.change": "Broyt privatverju av posti",
   "privacy.direct.long": "Øll, sum eru nevnd í postinum",
-  "privacy.direct.short": "Ávís fólk",
+  "privacy.direct.short": "Privat umrøða",
   "privacy.private.long": "Einans tey, ið fylgja tær",
   "privacy.private.short": "Fylgjarar",
   "privacy.public.long": "Øll í og uttanfyri Mastodon",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Broyt haldaramál fyri {target}",
   "tabs_bar.home": "Heim",
   "tabs_bar.notifications": "Fráboðanir",
+  "terms_of_service.effective_as_of": "Galdandi frá {date}",
   "terms_of_service.title": "Tænastutreytir",
+  "terms_of_service.upcoming_changes_on": "Komandi broytingar {date}",
   "time_remaining.days": "{number, plural, one {# dagur} other {# dagar}} eftir",
   "time_remaining.hours": "{number, plural, one {# tími} other {# tímar}} eftir",
   "time_remaining.minutes": "{number, plural, one {# minuttur} other {# minuttir}} eftir",
@@ -903,8 +912,12 @@
   "video.expand": "Víðka sjónfílu",
   "video.fullscreen": "Fullur skermur",
   "video.hide": "Fjal sjónfílu",
-  "video.mute": "Sløkk ljóðið",
+  "video.mute": "Doyv",
   "video.pause": "Steðga á",
   "video.play": "Spæl",
-  "video.unmute": "Tendra ljóðið"
+  "video.skip_backward": "Leyp um aftureftir",
+  "video.skip_forward": "Leyp um frameftir",
+  "video.unmute": "Doyv ikki",
+  "video.volume_down": "Minka ljóðstyrki",
+  "video.volume_up": "Øk um ljóðstyrki"
 }
diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json
index 2fddca9f16..f63a1d2cba 100644
--- a/app/javascript/mastodon/locales/fr-CA.json
+++ b/app/javascript/mastodon/locales/fr-CA.json
@@ -1,6 +1,6 @@
 {
   "about.blocks": "Serveurs modérés",
-  "about.contact": "Contact:",
+  "about.contact": "Contact :",
   "about.disclaimer": "Mastodon est un logiciel open-source gratuit et une marque déposée de Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Raison non disponible",
   "about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec des comptes de n'importe quel serveur dans le fediverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.",
@@ -27,11 +27,13 @@
   "account.edit_profile": "Modifier le profil",
   "account.enable_notifications": "Me notifier quand @{name} publie",
   "account.endorse": "Inclure sur profil",
+  "account.featured": "En vedette",
+  "account.featured.hashtags": "Hashtags",
+  "account.featured.posts": "Messages",
   "account.featured_tags.last_status_at": "Dernière publication {date}",
   "account.featured_tags.last_status_never": "Aucune publication",
-  "account.featured_tags.title": "Hashtags inclus de {name}",
   "account.follow": "Suivre",
-  "account.follow_back": "S'abonner en retour",
+  "account.follow_back": "Suivre en retour",
   "account.followers": "abonné·e·s",
   "account.followers.empty": "Personne ne suit ce compte pour l'instant.",
   "account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} message} other {{counter} messages}}",
   "account.unblock": "Débloquer @{name}",
   "account.unblock_domain": "Débloquer le domaine {domain}",
+  "account.unblock_domain_short": "Débloquer",
   "account.unblock_short": "Débloquer",
   "account.unendorse": "Ne pas inclure sur profil",
   "account.unfollow": "Ne plus suivre",
@@ -119,7 +122,7 @@
   "block_modal.show_less": "Afficher moins",
   "block_modal.show_more": "Afficher plus",
   "block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.",
-  "block_modal.they_cant_see_posts": "Il peut toujours voir vos messages, mais vous ne verrez pas les siens.",
+  "block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.",
   "block_modal.they_will_know": "Il peut voir qu'il est bloqué.",
   "block_modal.title": "Bloquer le compte ?",
   "block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.",
@@ -218,6 +221,10 @@
   "confirmations.logout.confirm": "Se déconnecter",
   "confirmations.logout.message": "Voulez-vous vraiment vous déconnecter?",
   "confirmations.logout.title": "Se déconnecter ?",
+  "confirmations.missing_alt_text.confirm": "Ajouter un texte alternatif",
+  "confirmations.missing_alt_text.message": "Votre post contient des médias sans texte alternatif. Ajouter des descriptions rend votre contenu accessible à un plus grand nombre de personnes.",
+  "confirmations.missing_alt_text.secondary": "Publier quand-même",
+  "confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
   "confirmations.mute.confirm": "Masquer",
   "confirmations.redraft.confirm": "Supprimer et réécrire",
   "confirmations.redraft.message": "Êtes-vous sûr·e de vouloir effacer cette publication pour la réécrire? Ses ses mises en favori et boosts seront perdus et ses réponses seront orphelines.",
@@ -289,6 +296,7 @@
   "emoji_button.search_results": "Résultats",
   "emoji_button.symbols": "Symboles",
   "emoji_button.travel": "Voyage et lieux",
+  "empty_column.account_featured": "Cette liste est vide",
   "empty_column.account_hides_collections": "Cet utilisateur·ice préfère ne pas rendre publiques ces informations",
   "empty_column.account_suspended": "Compte suspendu",
   "empty_column.account_timeline": "Aucune publication ici!",
@@ -693,7 +701,7 @@
   "poll_button.remove_poll": "Supprimer le sondage",
   "privacy.change": "Changer la confidentialité des messages",
   "privacy.direct.long": "Toutes les personnes mentionnées dans le post",
-  "privacy.direct.short": "Personnes spécifiques",
+  "privacy.direct.short": "Mention privée",
   "privacy.private.long": "Seulement vos abonnés",
   "privacy.private.short": "Abonnés",
   "privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
@@ -868,7 +876,9 @@
   "subscribed_languages.target": "Changer les langues abonnées pour {target}",
   "tabs_bar.home": "Accueil",
   "tabs_bar.notifications": "Notifications",
+  "terms_of_service.effective_as_of": "En vigueur à compter du {date}",
   "terms_of_service.title": "Conditions d'utilisation",
+  "terms_of_service.upcoming_changes_on": "Modifications à venir le {date}",
   "time_remaining.days": "{number, plural, one {# jour restant} other {# jours restants}}",
   "time_remaining.hours": "{number, plural, one {# heure restante} other {# heures restantes}}",
   "time_remaining.minutes": "{number, plural, one {# minute restante} other {# minutes restantes}}",
@@ -902,5 +912,7 @@
   "video.mute": "Couper le son",
   "video.pause": "Pause",
   "video.play": "Lecture",
-  "video.unmute": "Rétablir le son"
+  "video.unmute": "Rétablir le son",
+  "video.volume_down": "Baisser le volume",
+  "video.volume_up": "Augmenter le volume"
 }
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 1660132fed..f9c616627f 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -3,7 +3,7 @@
   "about.contact": "Contact :",
   "about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Raison non disponible",
-  "about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateur⋅rice⋅s de n'importe quel autre serveur dans le fédiverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.",
+  "about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateur⋅rices de n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur-là.",
   "about.domain_blocks.silenced.explanation": "Vous ne verrez généralement pas les profils et le contenu de ce serveur, à moins que vous ne les recherchiez explicitement ou que vous ne choisissiez de les suivre.",
   "about.domain_blocks.silenced.title": "Limité",
   "about.domain_blocks.suspended.explanation": "Aucune donnée de ce serveur ne sera traitée, enregistrée ou échangée, rendant impossible toute interaction ou communication avec les comptes de ce serveur.",
@@ -19,7 +19,7 @@
   "account.block_domain": "Bloquer le domaine {domain}",
   "account.block_short": "Bloquer",
   "account.blocked": "Bloqué·e",
-  "account.cancel_follow_request": "Annuler le suivi",
+  "account.cancel_follow_request": "Annuler l'abonnement",
   "account.copy": "Copier le lien vers le profil",
   "account.direct": "Mention privée @{name}",
   "account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose",
@@ -27,22 +27,24 @@
   "account.edit_profile": "Modifier le profil",
   "account.enable_notifications": "Me notifier quand @{name} publie quelque chose",
   "account.endorse": "Recommander sur votre profil",
+  "account.featured": "En vedette",
+  "account.featured.hashtags": "Hashtags",
+  "account.featured.posts": "Messages",
   "account.featured_tags.last_status_at": "Dernier message le {date}",
   "account.featured_tags.last_status_never": "Aucun message",
-  "account.featured_tags.title": "Les hashtags en vedette de {name}",
   "account.follow": "Suivre",
-  "account.follow_back": "S'abonner en retour",
+  "account.follow_back": "Suivre en retour",
   "account.followers": "Abonné·e·s",
   "account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour l’instant.",
   "account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
   "account.following": "Abonnements",
   "account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}",
   "account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.",
-  "account.go_to_profile": "Aller au profil",
+  "account.go_to_profile": "Voir le profil",
   "account.hide_reblogs": "Masquer les partages de @{name}",
   "account.in_memoriam": "En mémoire de.",
   "account.joined_short": "Ici depuis",
-  "account.languages": "Changer les langues abonnées",
+  "account.languages": "Modifier les langues d'abonnements",
   "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}",
   "account.locked_info": "Ce compte est privé. Son ou sa propriétaire approuve manuellement qui peut le suivre.",
   "account.media": "Médias",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} message} other {{counter} messages}}",
   "account.unblock": "Débloquer @{name}",
   "account.unblock_domain": "Débloquer le domaine {domain}",
+  "account.unblock_domain_short": "Débloquer",
   "account.unblock_short": "Débloquer",
   "account.unendorse": "Ne plus recommander sur le profil",
   "account.unfollow": "Ne plus suivre",
@@ -119,7 +122,7 @@
   "block_modal.show_less": "Afficher moins",
   "block_modal.show_more": "Afficher plus",
   "block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.",
-  "block_modal.they_cant_see_posts": "Il peut toujours voir vos messages, mais vous ne verrez pas les siens.",
+  "block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.",
   "block_modal.they_will_know": "Il peut voir qu'il est bloqué.",
   "block_modal.title": "Bloquer le compte ?",
   "block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.",
@@ -218,6 +221,10 @@
   "confirmations.logout.confirm": "Se déconnecter",
   "confirmations.logout.message": "Voulez-vous vraiment vous déconnecter ?",
   "confirmations.logout.title": "Se déconnecter ?",
+  "confirmations.missing_alt_text.confirm": "Ajouter un texte alternatif",
+  "confirmations.missing_alt_text.message": "Votre post contient des médias sans texte alternatif. Ajouter des descriptions rend votre contenu accessible à un plus grand nombre de personnes.",
+  "confirmations.missing_alt_text.secondary": "Publier quand-même",
+  "confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
   "confirmations.mute.confirm": "Masquer",
   "confirmations.redraft.confirm": "Supprimer et ré-écrire",
   "confirmations.redraft.message": "Voulez-vous vraiment supprimer le message pour le réécrire ? Ses partages ainsi que ses mises en favori seront perdues, et ses réponses seront orphelines.",
@@ -289,6 +296,7 @@
   "emoji_button.search_results": "Résultats de la recherche",
   "emoji_button.symbols": "Symboles",
   "emoji_button.travel": "Voyage et lieux",
+  "empty_column.account_featured": "Cette liste est vide",
   "empty_column.account_hides_collections": "Cet utilisateur·ice préfère ne pas rendre publiques ces informations",
   "empty_column.account_suspended": "Compte suspendu",
   "empty_column.account_timeline": "Aucun message ici !",
@@ -693,7 +701,7 @@
   "poll_button.remove_poll": "Supprimer le sondage",
   "privacy.change": "Ajuster la confidentialité du message",
   "privacy.direct.long": "Toutes les personnes mentionnées dans le post",
-  "privacy.direct.short": "Personnes spécifiques",
+  "privacy.direct.short": "Mention privée",
   "privacy.private.long": "Seulement vos abonnés",
   "privacy.private.short": "Abonnés",
   "privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
@@ -868,7 +876,9 @@
   "subscribed_languages.target": "Changer les langues abonnées pour {target}",
   "tabs_bar.home": "Accueil",
   "tabs_bar.notifications": "Notifications",
+  "terms_of_service.effective_as_of": "En vigueur à compter du {date}",
   "terms_of_service.title": "Conditions d'utilisation",
+  "terms_of_service.upcoming_changes_on": "Modifications à venir le {date}",
   "time_remaining.days": "{number, plural, one {# jour restant} other {# jours restants}}",
   "time_remaining.hours": "{number, plural, one {# heure restante} other {# heures restantes}}",
   "time_remaining.minutes": "{number, plural, one {# minute restante} other {# minutes restantes}}",
@@ -902,5 +912,7 @@
   "video.mute": "Couper le son",
   "video.pause": "Pause",
   "video.play": "Lecture",
-  "video.unmute": "Rétablir le son"
+  "video.unmute": "Rétablir le son",
+  "video.volume_down": "Baisser le volume",
+  "video.volume_up": "Augmenter le volume"
 }
diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json
index aafef2a41d..e3c3222868 100644
--- a/app/javascript/mastodon/locales/fy.json
+++ b/app/javascript/mastodon/locales/fy.json
@@ -29,7 +29,6 @@
   "account.endorse": "Op profyl werjaan",
   "account.featured_tags.last_status_at": "Lêste berjocht op {date}",
   "account.featured_tags.last_status_never": "Gjin berjochten",
-  "account.featured_tags.title": "Utljochte hashtags fan {name}",
   "account.follow": "Folgje",
   "account.follow_back": "Weromfolgje",
   "account.followers": "Folgers",
@@ -682,7 +681,6 @@
   "poll_button.remove_poll": "Enkête fuortsmite",
   "privacy.change": "Sichtberheid fan berjocht oanpasse",
   "privacy.direct.long": "Elkenien dy’ yn it berjocht fermeld wurdt",
-  "privacy.direct.short": "Bepaalde minsken",
   "privacy.private.long": "Allinnich jo folgers",
   "privacy.private.short": "Folgers",
   "privacy.public.long": "Elkenien op Mastodon en dêrbûten",
@@ -888,8 +886,6 @@
   "video.expand": "Fideo grutter meitsje",
   "video.fullscreen": "Folslein skerm",
   "video.hide": "Fideo ferstopje",
-  "video.mute": "Lûd dôvje",
   "video.pause": "Skoft",
-  "video.play": "Ofspylje",
-  "video.unmute": "Lûd oan"
+  "video.play": "Ofspylje"
 }
diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json
index 2e899b2804..5935b39e2d 100644
--- a/app/javascript/mastodon/locales/ga.json
+++ b/app/javascript/mastodon/locales/ga.json
@@ -29,7 +29,6 @@
   "account.endorse": "Cuir ar an phróifíl mar ghné",
   "account.featured_tags.last_status_at": "Postáil is déanaí ar {date}",
   "account.featured_tags.last_status_never": "Gan aon phoist",
-  "account.featured_tags.title": "Haischlib faoi thrácht {name}",
   "account.follow": "Lean",
   "account.follow_back": "Leanúint ar ais",
   "account.followers": "Leantóirí",
@@ -65,6 +64,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} poist}}",
   "account.unblock": "Bain bac de @{name}",
   "account.unblock_domain": "Bain bac den ainm fearainn {domain}",
+  "account.unblock_domain_short": "Díbhlocáil",
   "account.unblock_short": "Díbhlocáil",
   "account.unendorse": "Ná chuir ar an phróifíl mar ghné",
   "account.unfollow": "Ná lean a thuilleadh",
@@ -86,6 +86,13 @@
   "alert.unexpected.message": "Tharla earráid gan choinne.",
   "alert.unexpected.title": "Hiúps!",
   "alt_text_badge.title": "Téacs alt",
+  "alt_text_modal.add_alt_text": "Cuir téacs alt leis",
+  "alt_text_modal.add_text_from_image": "Cuir téacs ón íomhá leis",
+  "alt_text_modal.cancel": "Cealaigh",
+  "alt_text_modal.change_thumbnail": "Athraigh mionsamhail",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Déan cur síos air seo do dhaoine le lagú éisteachta…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Déan cur síos air seo do dhaoine a bhfuil lagú amhairc orthu…",
+  "alt_text_modal.done": "Déanta",
   "announcement.announcement": "Fógra",
   "annual_report.summary.archetype.booster": "An sealgair fionnuar",
   "annual_report.summary.archetype.lurker": "An lurker",
@@ -211,6 +218,10 @@
   "confirmations.logout.confirm": "Logáil amach",
   "confirmations.logout.message": "An bhfuil tú cinnte gur mhaith leat logáil amach?",
   "confirmations.logout.title": "Logáil Amach?",
+  "confirmations.missing_alt_text.confirm": "Cuir téacs alt leis",
+  "confirmations.missing_alt_text.message": "Tá meáin gan alt téacs i do phostáil. Má chuirtear tuairiscí leis, cabhraíonn sé seo leat d’inneachar a rochtain do níos mó daoine.",
+  "confirmations.missing_alt_text.secondary": "Post ar aon nós",
+  "confirmations.missing_alt_text.title": "Cuir téacs alt leis?",
   "confirmations.mute.confirm": "Balbhaigh",
   "confirmations.redraft.confirm": "Scrios ⁊ athdhréachtaigh",
   "confirmations.redraft.message": "An bhfuil tú cinnte gur mhaith leat an postáil seo a scriosadh agus é a athdhréachtú? Caillfear ceanáin agus treisithe, agus dílleachtaí freagraí ar an mbunphostála.",
@@ -407,6 +418,8 @@
   "ignore_notifications_modal.not_followers_title": "An dtugann tú aird ar fhógraí ó dhaoine nach leanann tú?",
   "ignore_notifications_modal.not_following_title": "An ndéanann tú neamhaird de fhógraí ó dhaoine nach leanann tú?",
   "ignore_notifications_modal.private_mentions_title": "An dtugann tú aird ar fhógraí ó Luaintí Príobháideacha gan iarraidh?",
+  "info_button.label": "Cabhrú",
+  "info_button.what_is_alt_text": "<h1>Cad is téacs altach ann?</h1> <p>Soláthraíonn téacs Alt cur síos ar íomhánna do dhaoine le lagú radhairc, naisc íseal-bandaleithead, nó daoine atá ag lorg comhthéacs breise.</p> <p>Is féidir leat inrochtaineacht agus tuiscint a fheabhsú do chách trí théacs alt soiléir, gonta, oibiachtúil a scríobh.</p> <ul> <li>Glac gnéithe tábhachtacha</li> <li>Déan achoimre ar théacs in íomhánna</li> <li>Úsáid struchtúr abairtí rialta</li> li> <li>Seachain faisnéis iomarcach</li> <li>Fócas ar threochtaí agus ar phríomhthorthaí i bhfíseanna casta (amhail léaráidí nó léarscáileanna)</li> </ul>",
   "interaction_modal.action.favourite": "Chun leanúint ar aghaidh, ní mór duit an ceann is fearr leat ó do chuntas.",
   "interaction_modal.action.follow": "Chun leanúint ar aghaidh, ní mór duit leanúint ó do chuntas.",
   "interaction_modal.action.reblog": "Chun leanúint ar aghaidh, ní mór duit athbhlagáil ó do chuntas.",
@@ -684,7 +697,7 @@
   "poll_button.remove_poll": "Bain suirbhé",
   "privacy.change": "Athraigh príobháideacht postála",
   "privacy.direct.long": "Luaigh gach duine sa phost",
-  "privacy.direct.short": "Daoine ar leith",
+  "privacy.direct.short": "Tagairt phríobháideach",
   "privacy.private.long": "Do leanúna amháin",
   "privacy.private.short": "Leantóirí",
   "privacy.public.long": "Duine ar bith ar agus amach Mastodon",
@@ -859,7 +872,9 @@
   "subscribed_languages.target": "Athraigh teangacha suibscríofa le haghaidh {target}",
   "tabs_bar.home": "Baile",
   "tabs_bar.notifications": "Fógraí",
+  "terms_of_service.effective_as_of": "I bhfeidhm ó {date}",
   "terms_of_service.title": "Téarmaí Seirbhíse",
+  "terms_of_service.upcoming_changes_on": "Athruithe atá le teacht ar {date}",
   "time_remaining.days": "{number, plural, one {# lá} other {# lá}} fágtha",
   "time_remaining.hours": "{number, plural, one {# uair} other {# uair}} fágtha",
   "time_remaining.minutes": "{number, plural, one {# nóiméad} other {# nóiméad}} fágtha",
@@ -890,8 +905,12 @@
   "video.expand": "Leath físeán",
   "video.fullscreen": "Lánscáileán",
   "video.hide": "Cuir físeán i bhfolach",
-  "video.mute": "Ciúnaigh fuaim",
+  "video.mute": "Balbhaigh",
   "video.pause": "Cuir ar sos",
   "video.play": "Cuir ar siúl",
-  "video.unmute": "Díchiúnaigh fuaim"
+  "video.skip_backward": "Scipeáil siar",
+  "video.skip_forward": "Scipeáil ar aghaidh",
+  "video.unmute": "Díbhalbhú",
+  "video.volume_down": "Toirt síos",
+  "video.volume_up": "Toirt suas"
 }
diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json
index b2b6e6f9b3..f295db0b43 100644
--- a/app/javascript/mastodon/locales/gd.json
+++ b/app/javascript/mastodon/locales/gd.json
@@ -29,7 +29,6 @@
   "account.endorse": "Brosnaich air a’ phròifil",
   "account.featured_tags.last_status_at": "Am post mu dheireadh {date}",
   "account.featured_tags.last_status_never": "Gun phost",
-  "account.featured_tags.title": "Na tagaichean hais brosnaichte aig {name}",
   "account.follow": "Lean",
   "account.follow_back": "Lean air ais",
   "account.followers": "Luchd-leantainn",
@@ -633,7 +632,6 @@
   "poll_button.remove_poll": "Thoir air falbh an cunntas-bheachd",
   "privacy.change": "Cuir gleus air prìobhaideachd a’ phuist",
   "privacy.direct.long": "A h-uile duine air a bheil iomradh sa phost",
-  "privacy.direct.short": "Daoine àraidh",
   "privacy.private.long": "An luchd-leantainn agad a-mhàin",
   "privacy.private.short": "Luchd-leantainn",
   "privacy.public.long": "Duine sam bith taobh a-staigh no a-muigh Mhastodon",
@@ -832,8 +830,6 @@
   "video.expand": "Leudaich a’ video",
   "video.fullscreen": "Làn-sgrìn",
   "video.hide": "Falaich a’ video",
-  "video.mute": "Mùch an fhuaim",
   "video.pause": "Cuir ’na stad",
-  "video.play": "Cluich",
-  "video.unmute": "Dì-mhùch an fhuaim"
+  "video.play": "Cluich"
 }
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index b26e977609..50dc2437c6 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Editar perfil",
   "account.enable_notifications": "Noficarme cando @{name} publique",
   "account.endorse": "Amosar no perfil",
+  "account.featured": "Destacado",
+  "account.featured.hashtags": "Cancelos",
+  "account.featured.posts": "Publicacións",
   "account.featured_tags.last_status_at": "Última publicación o {date}",
   "account.featured_tags.last_status_never": "Sen publicacións",
-  "account.featured_tags.title": "Cancelos destacados de {name}",
   "account.follow": "Seguir",
   "account.follow_back": "Seguir tamén",
   "account.followers": "Seguidoras",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Amosar {domain}",
+  "account.unblock_domain_short": "Desbloquear",
   "account.unblock_short": "Desbloquear",
   "account.unendorse": "Non amosar no perfil",
   "account.unfollow": "Deixar de seguir",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Resultados da procura",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viaxes e Lugares",
+  "empty_column.account_featured": "A lista está baleira",
   "empty_column.account_hides_collections": "A usuaria decideu non facer pública esta información",
   "empty_column.account_suspended": "Conta suspendida",
   "empty_column.account_timeline": "Non hai publicacións aquí!",
@@ -377,6 +381,8 @@
   "generic.saved": "Gardado",
   "getting_started.heading": "Primeiros pasos",
   "hashtag.admin_moderation": "Abrir interface de moderación para ##{name}",
+  "hashtag.browse": "Ver publicacións con #{hashtag}",
+  "hashtag.browse_from_account": "Ver as publicacións de @{name} con #{hashtag}",
   "hashtag.column_header.tag_mode.all": "e {additional}",
   "hashtag.column_header.tag_mode.any": "ou {additional}",
   "hashtag.column_header.tag_mode.none": "sen {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicacións}} hoxe",
   "hashtag.follow": "Seguir cancelo",
+  "hashtag.mute": "Acalar a #{hashtag}",
   "hashtag.unfollow": "Deixar de seguir cancelo",
   "hashtags.and_other": "…e {count, plural, one {}other {# máis}}",
   "hints.profiles.followers_may_be_missing": "Poderían faltar seguidoras deste perfil.",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Eliminar enquisa",
   "privacy.change": "Axustar privacidade",
   "privacy.direct.long": "Todas as mencionadas na publicación",
-  "privacy.direct.short": "Persoas mencionadas",
+  "privacy.direct.short": "Mención privada",
   "privacy.private.long": "Só para seguidoras",
   "privacy.private.short": "Seguidoras",
   "privacy.public.long": "Para todas dentro e fóra de Mastodon",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Cambiar a subscrición a idiomas para {target}",
   "tabs_bar.home": "Inicio",
   "tabs_bar.notifications": "Notificacións",
+  "terms_of_service.effective_as_of": "Con efecto desde o {date}",
   "terms_of_service.title": "Termos do Servizo",
+  "terms_of_service.upcoming_changes_on": "Cambios por vir o {date}",
   "time_remaining.days": "Remata en {number, plural, one {# día} other {# días}}",
   "time_remaining.hours": "Remata en {number, plural, one {# hora} other {# horas}}",
   "time_remaining.minutes": "Remata en {number, plural, one {# minuto} other {# minutos}}",
@@ -903,8 +912,12 @@
   "video.expand": "Estender o vídeo",
   "video.fullscreen": "Pantalla completa",
   "video.hide": "Agochar vídeo",
-  "video.mute": "Silenciar son",
+  "video.mute": "Acalar",
   "video.pause": "Deter",
   "video.play": "Reproducir",
-  "video.unmute": "Permitir son"
+  "video.skip_backward": "Retroceder",
+  "video.skip_forward": "Avanzar",
+  "video.unmute": "Non silenciar",
+  "video.volume_down": "Baixar volume",
+  "video.volume_up": "Subir volume"
 }
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 2358d7bd2a..d1a2c014a5 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "עריכת פרופיל",
   "account.enable_notifications": "שלח לי התראות כש@{name} מפרסם",
   "account.endorse": "קדם את החשבון בפרופיל",
+  "account.featured": "מומלץ",
+  "account.featured.hashtags": "תגיות",
+  "account.featured.posts": "הודעות",
   "account.featured_tags.last_status_at": "חצרוץ אחרון בתאריך {date}",
   "account.featured_tags.last_status_never": "אין חצרוצים",
-  "account.featured_tags.title": "התגיות המועדפות של {name}",
   "account.follow": "לעקוב",
   "account.follow_back": "לעקוב בחזרה",
   "account.followers": "עוקבים",
@@ -64,8 +66,9 @@
   "account.show_reblogs": "הצג הדהודים מאת @{name}",
   "account.statuses_counter": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}}",
   "account.unblock": "להסיר חסימה ל- @{name}",
-  "account.unblock_domain": "הסירי את החסימה של קהילת {domain}",
-  "account.unblock_short": "הסר חסימה",
+  "account.unblock_domain": "הסרת החסימה של קהילת {domain}",
+  "account.unblock_domain_short": "הסרת חסימה",
+  "account.unblock_short": "הסרת חסימה",
   "account.unendorse": "אל תקדם בפרופיל",
   "account.unfollow": "הפסקת מעקב",
   "account.unmute": "הפסקת השתקת @{name}",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "תוצאות חיפוש",
   "emoji_button.symbols": "סמלים",
   "emoji_button.travel": "טיולים ואתרים",
+  "empty_column.account_featured": "הרשימה ריקה",
   "empty_column.account_hides_collections": "המשתמש.ת בחר.ה להסתיר מידע זה",
   "empty_column.account_suspended": "חשבון מושעה",
   "empty_column.account_timeline": "אין עדיין אף הודעה!",
@@ -377,6 +381,8 @@
   "generic.saved": "נשמר",
   "getting_started.heading": "בואו נתחיל",
   "hashtag.admin_moderation": "פתיחת ממשק פיקוח דיון עבור #{name}",
+  "hashtag.browse": "קריאת הודעות תחת #{hashtag}",
+  "hashtag.browse_from_account": "קריאת הודעות מאת @{name} תחת #{hashtag}",
   "hashtag.column_header.tag_mode.all": "ו- {additional}",
   "hashtag.column_header.tag_mode.any": "או {additional}",
   "hashtag.column_header.tag_mode.none": "ללא {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}} היום",
   "hashtag.follow": "לעקוב אחרי תגית",
+  "hashtag.mute": "להשתיק את #{hashtag}",
   "hashtag.unfollow": "להפסיק לעקוב אחרי תגית",
   "hashtags.and_other": "…{count, plural,other {ועוד #}}",
   "hints.profiles.followers_may_be_missing": "יתכן כי עוקבים של פרופיל זה חסרים.",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "הסרת סקר",
   "privacy.change": "שינוי פרטיות ההודעה",
   "privacy.direct.long": "כל המוזכרים בהודעה",
-  "privacy.direct.short": "א.נשים מסוימים",
+  "privacy.direct.short": "אזכור פרטי",
   "privacy.private.long": "לעוקביך בלבד",
   "privacy.private.short": "עוקבים",
   "privacy.public.long": "כל הגולשים, מחוברים למסטודון או לא",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "שינוי רישום שפה עבור {target}",
   "tabs_bar.home": "פיד הבית",
   "tabs_bar.notifications": "התראות",
+  "terms_of_service.effective_as_of": "בתוקף החל מתאריך {date}",
   "terms_of_service.title": "תנאי השירות",
+  "terms_of_service.upcoming_changes_on": "שינויים עתידיים שיחולו ביום {date}",
   "time_remaining.days": "נותרו {number, plural, one {# יום} other {# ימים}}",
   "time_remaining.hours": "נותרו {number, plural, one {# שעה} other {# שעות}}",
   "time_remaining.minutes": "נותרו {number, plural, one {# דקה} other {# דקות}}",
@@ -903,8 +912,12 @@
   "video.expand": "להרחיב וידאו",
   "video.fullscreen": "מסך מלא",
   "video.hide": "להסתיר וידאו",
-  "video.mute": "השתקת צליל",
+  "video.mute": "השתקה",
   "video.pause": "השהיה",
   "video.play": "ניגון",
-  "video.unmute": "החזרת צליל"
+  "video.skip_backward": "דילוג אחורה",
+  "video.skip_forward": "דילוג קדימה",
+  "video.unmute": "ביטול השתקה",
+  "video.volume_down": "הנמכת עוצמת השמע",
+  "video.volume_up": "הגברת עוצמת שמע"
 }
diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json
index 10c5356719..a3eec9544c 100644
--- a/app/javascript/mastodon/locales/hi.json
+++ b/app/javascript/mastodon/locales/hi.json
@@ -28,7 +28,6 @@
   "account.endorse": "प्रोफ़ाइल पर दिखाए",
   "account.featured_tags.last_status_at": "{date} का अंतिम पोस्ट",
   "account.featured_tags.last_status_never": "कोई पोस्ट नहीं है",
-  "account.featured_tags.title": "{name} के चुनिंदा हैशटैग",
   "account.follow": "फॉलो करें",
   "account.follow_back": "फॉलो करें",
   "account.followers": "फॉलोवर",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index c4afdec3ab..38807b28b2 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -28,7 +28,6 @@
   "account.endorse": "Istakni na profilu",
   "account.featured_tags.last_status_at": "Zadnji post {date}",
   "account.featured_tags.last_status_never": "Nema postova",
-  "account.featured_tags.title": "Istaknuti hashtagovi {name}",
   "account.follow": "Prati",
   "account.follow_back": "Slijedi natrag",
   "account.followers": "Pratitelji",
@@ -483,8 +482,6 @@
   "video.expand": "Proširi video",
   "video.fullscreen": "Cijeli zaslon",
   "video.hide": "Sakrij video",
-  "video.mute": "Utišaj zvuk",
   "video.pause": "Pauziraj",
-  "video.play": "Reproduciraj",
-  "video.unmute": "Uključi zvuk"
+  "video.play": "Reproduciraj"
 }
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 960f6b535b..b311dffa72 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Profil szerkesztése",
   "account.enable_notifications": "Figyelmeztessen, ha @{name} bejegyzést tesz közzé",
   "account.endorse": "Kiemelés a profilodon",
+  "account.featured": "Kiemelt",
+  "account.featured.hashtags": "Hashtagek",
+  "account.featured.posts": "Bejegyzések",
   "account.featured_tags.last_status_at": "Legutolsó bejegyzés ideje: {date}",
   "account.featured_tags.last_status_never": "Nincs bejegyzés",
-  "account.featured_tags.title": "{name} kiemelt hashtagjei",
   "account.follow": "Követés",
   "account.follow_back": "Viszontkövetés",
   "account.followers": "Követő",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} bejegyzés} other {{counter} bejegyzés}}",
   "account.unblock": "@{name} letiltásának feloldása",
   "account.unblock_domain": "{domain} domain tiltásának feloldása",
+  "account.unblock_domain_short": "Tiltás feloldása",
   "account.unblock_short": "Tiltás feloldása",
   "account.unendorse": "Ne jelenjen meg a profilodon",
   "account.unfollow": "Követés megszüntetése",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Keresési találatok",
   "emoji_button.symbols": "Szimbólumok",
   "emoji_button.travel": "Utazás és helyek",
+  "empty_column.account_featured": "Ez a lista üres",
   "empty_column.account_hides_collections": "Ez a felhasználó úgy döntött, hogy nem teszi elérhetővé ezt az információt.",
   "empty_column.account_suspended": "Fiók felfüggesztve",
   "empty_column.account_timeline": "Itt nincs bejegyzés!",
@@ -377,6 +381,8 @@
   "generic.saved": "Elmentve",
   "getting_started.heading": "Első lépések",
   "hashtag.admin_moderation": "Moderációs felület megnyitása a következőhöz: #{name}",
+  "hashtag.browse": "Bejegyzések ebben: #{hashtag}",
+  "hashtag.browse_from_account": "@{name} bejegyzéseinek tallózása ebben: #{hashtag}",
   "hashtag.column_header.tag_mode.all": "és {additional}",
   "hashtag.column_header.tag_mode.any": "vagy {additional}",
   "hashtag.column_header.tag_mode.none": "{additional} nélkül",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} bejegyzés} other {{counter} bejegyzés}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} bejegyzés} other {{counter} bejegyzés}} ma",
   "hashtag.follow": "Hashtag követése",
+  "hashtag.mute": "#{hashtag} némítása",
   "hashtag.unfollow": "Hashtag követésének megszüntetése",
   "hashtags.and_other": "…és {count, plural, other {# további}}",
   "hints.profiles.followers_may_be_missing": "A profil követői lehet, hogy hiányoznak.",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Szavazás eltávolítása",
   "privacy.change": "Bejegyzés láthatóságának módosítása",
   "privacy.direct.long": "Mindenki, akit a bejegyzés említ",
-  "privacy.direct.short": "Megadott személyek",
+  "privacy.direct.short": "Privát említés",
   "privacy.private.long": "Csak a követőid",
   "privacy.private.short": "Követők",
   "privacy.public.long": "Bárki a Mastodonon és azon kívül",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Feliratkozott nyelvek módosítása {target} esetében",
   "tabs_bar.home": "Kezdőlap",
   "tabs_bar.notifications": "Értesítések",
+  "terms_of_service.effective_as_of": "Hatálybalépés dátuma: {date}",
   "terms_of_service.title": "Felhasználási feltételek",
+  "terms_of_service.upcoming_changes_on": "Érkező változások: {date}",
   "time_remaining.days": "{number, plural, one {# nap} other {# nap}} van hátra",
   "time_remaining.hours": "{number, plural, one {# óra} other {# óra}} van hátra",
   "time_remaining.minutes": "{number, plural, one {# perc} other {# perc}} van hátra",
@@ -903,8 +912,12 @@
   "video.expand": "Videó nagyítása",
   "video.fullscreen": "Teljes képernyő",
   "video.hide": "Videó elrejtése",
-  "video.mute": "Hang némítása",
+  "video.mute": "Némítás",
   "video.pause": "Szünet",
   "video.play": "Lejátszás",
-  "video.unmute": "Hang némításának feloldása"
+  "video.skip_backward": "Visszaugrás",
+  "video.skip_forward": "Előreugrás",
+  "video.unmute": "Némítás feloldása",
+  "video.volume_down": "Hangerő le",
+  "video.volume_up": "Hangerő fel"
 }
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 7786e41f6a..9882d575b2 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -473,8 +473,6 @@
   "video.expand": "Ընդարձակել տեսագրութիւնը",
   "video.fullscreen": "Լիաէկրան",
   "video.hide": "Թաքցնել տեսագրութիւնը",
-  "video.mute": "Լռեցնել ձայնը",
   "video.pause": "Դադար տալ",
-  "video.play": "Նուագել",
-  "video.unmute": "Միացնել ձայնը"
+  "video.play": "Նուագել"
 }
diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json
index fc3194e4dd..7f4f66796e 100644
--- a/app/javascript/mastodon/locales/ia.json
+++ b/app/javascript/mastodon/locales/ia.json
@@ -29,7 +29,6 @@
   "account.endorse": "Evidentiar sur le profilo",
   "account.featured_tags.last_status_at": "Ultime message publicate le {date}",
   "account.featured_tags.last_status_never": "Necun message",
-  "account.featured_tags.title": "Hashtags eminente de {name}",
   "account.follow": "Sequer",
   "account.follow_back": "Sequer in retorno",
   "account.followers": "Sequitores",
@@ -86,9 +85,12 @@
   "alert.unexpected.message": "Un error inexpectate ha occurrite.",
   "alert.unexpected.title": "Ups!",
   "alt_text_badge.title": "Texto alternative",
+  "alt_text_modal.add_alt_text": "Adder texto alternative",
   "alt_text_modal.add_text_from_image": "Adder texto ab imagine",
   "alt_text_modal.cancel": "Cancellar",
   "alt_text_modal.change_thumbnail": "Cambiar le miniatura",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Describe isto pro personas con impedimentos auditive…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Describe isto pro personas con impedimentos visual…",
   "alt_text_modal.done": "Preste",
   "announcement.announcement": "Annuncio",
   "annual_report.summary.archetype.booster": "Le impulsator",
@@ -215,6 +217,10 @@
   "confirmations.logout.confirm": "Clauder session",
   "confirmations.logout.message": "Es tu secur que tu vole clauder le session?",
   "confirmations.logout.title": "Clauder session?",
+  "confirmations.missing_alt_text.confirm": "Adder texto alternative",
+  "confirmations.missing_alt_text.message": "Tu message contine multimedia sin texto alternative. Adder descriptiones adjuta a render tu contento accessibile a plus personas.",
+  "confirmations.missing_alt_text.secondary": "Publicar totevia",
+  "confirmations.missing_alt_text.title": "Adder texto alternative?",
   "confirmations.mute.confirm": "Silentiar",
   "confirmations.redraft.confirm": "Deler e rescriber",
   "confirmations.redraft.message": "Es tu secur de voler deler iste message e rescriber lo? Le favorites e le impulsos essera perdite, e le responsas al message original essera orphanate.",
@@ -299,7 +305,7 @@
   "empty_column.favourited_statuses": "Tu non ha alcun message favorite ancora. Quando tu marca un message como favorite, illo apparera hic.",
   "empty_column.favourites": "Necuno ha ancora marcate iste message como favorite. Quando alcuno lo face, ille apparera hic.",
   "empty_column.follow_requests": "Tu non ha ancora requestas de sequimento. Quando tu recipe un, illo apparera hic.",
-  "empty_column.followed_tags": "Tu non ha ancora sequite alcun hashtags. Quando tu lo face, illos apparera hic.",
+  "empty_column.followed_tags": "Tu non seque ancora alcun hashtags. Quando tu lo face, illos apparera hic.",
   "empty_column.hashtag": "Il non ha ancora alcun cosa in iste hashtag.",
   "empty_column.home": "Tu chronologia de initio es vacue! Seque plus personas pro plenar lo.",
   "empty_column.list": "Iste lista es ancora vacue. Quando le membros de iste lista publica nove messages, illos apparera hic.",
@@ -351,11 +357,11 @@
   "follow_suggestions.hints.friends_of_friends": "Iste profilo es popular inter le gente que tu seque.",
   "follow_suggestions.hints.most_followed": "Iste profilo es un del plus sequites sur {domain}.",
   "follow_suggestions.hints.most_interactions": "Iste profilo ha recentemente recipite multe attention sur {domain}.",
-  "follow_suggestions.hints.similar_to_recently_followed": "Iste profilo es similar al profilos que tu ha recentemente sequite.",
+  "follow_suggestions.hints.similar_to_recently_followed": "Iste profilo es similar al profilos que tu ha recentemente comenciate a sequer.",
   "follow_suggestions.personalized_suggestion": "Suggestion personalisate",
   "follow_suggestions.popular_suggestion": "Suggestion personalisate",
   "follow_suggestions.popular_suggestion_longer": "Popular sur {domain}",
-  "follow_suggestions.similar_to_recently_followed_longer": "Similar al profilos que tu ha sequite recentemente",
+  "follow_suggestions.similar_to_recently_followed_longer": "Similar al profilos que tu ha recentemente comenciate a sequer",
   "follow_suggestions.view_all": "Vider toto",
   "follow_suggestions.who_to_follow": "Qui sequer",
   "followed_tags": "Hashtags sequite",
@@ -411,6 +417,8 @@
   "ignore_notifications_modal.not_followers_title": "Ignorar notificationes de personas qui non te seque?",
   "ignore_notifications_modal.not_following_title": "Ignorar notificationes de personas que tu non seque?",
   "ignore_notifications_modal.private_mentions_title": "Ignorar notificationes de mentiones private non requestate?",
+  "info_button.label": "Adjuta",
+  "info_button.what_is_alt_text": "<h1>Que es texto alternative?</h1><p>Le texto alternative forni descriptiones de imagines a personas con impedimentos visual, con connexiones lente, o qui cerca contexto additional.</p><p>Tu pote meliorar le accessibilitate e le comprension pro totes scribente un texto alternative clar, concise e objective.</p><ul><li>Captura le elementos importante</li><li>Summarisa texto in imagines</li><li>Usa le structura de phrase normal</li><li>Evita information redundante</li><li>In figuras complexe (como diagrammas o mappas), concentra te sur le tendentias e punctos clave</li></ul>",
   "interaction_modal.action.favourite": "Per favor reveni a tu conto pro marcar isto como favorite.",
   "interaction_modal.action.follow": "Per favor reveni a tu conto pro sequer.",
   "interaction_modal.action.reblog": "Per favor reveni a tu conto pro impulsar.",
@@ -554,8 +562,8 @@
   "notification.favourite.name_and_others_with_link": "{name} e <a>{count, plural, one {# altere} other {# alteres}}</a> ha marcate tu message como favorite",
   "notification.favourite_pm": "{name} ha marcate tu mention privte como favorite",
   "notification.favourite_pm.name_and_others_with_link": "{name} e <a>{count, plural, one {# altere} other {# alteres}}</a> ha marcate tu mention private como favorite",
-  "notification.follow": "{name} te ha sequite",
-  "notification.follow.name_and_others": "{name} e <a>{count, plural, one {# other} other {# alteres}}</a> te ha sequite",
+  "notification.follow": "{name} te seque",
+  "notification.follow.name_and_others": "{name} e <a>{count, plural, one {# other} other {# alteres}}</a> te seque",
   "notification.follow_request": "{name} ha requestate de sequer te",
   "notification.follow_request.name_and_others": "{name} e {count, plural, one {# altere} other {# alteres}} ha demandate de sequer te",
   "notification.label.mention": "Mention",
@@ -648,7 +656,7 @@
   "notifications.policy.filter_limited_accounts_title": "Contos moderate",
   "notifications.policy.filter_new_accounts.hint": "Create in le ultime {days, plural, one {die} other {# dies}}",
   "notifications.policy.filter_new_accounts_title": "Nove contos",
-  "notifications.policy.filter_not_followers_hint": "Includente le personas que te ha sequite durante minus de {days, plural, one {un die} other {# dies}}",
+  "notifications.policy.filter_not_followers_hint": "Includente le personas que te seque desde minus de {days, plural, one {un die} other {# dies}}",
   "notifications.policy.filter_not_followers_title": "Personas qui non te seque",
   "notifications.policy.filter_not_following_hint": "Usque tu les approba manualmente",
   "notifications.policy.filter_not_following_title": "Personas que tu non seque",
@@ -688,7 +696,7 @@
   "poll_button.remove_poll": "Remover un sondage",
   "privacy.change": "Cambiar le confidentialitate del message",
   "privacy.direct.long": "Tote le personas mentionate in le message",
-  "privacy.direct.short": "Personas specific",
+  "privacy.direct.short": "Mention private",
   "privacy.private.long": "Solmente tu sequitores",
   "privacy.private.short": "Sequitores",
   "privacy.public.long": "Quicunque, sur Mastodon o non",
@@ -825,7 +833,7 @@
   "status.load_more": "Cargar plus",
   "status.media.open": "Clicca pro aperir",
   "status.media.show": "Clicca pro monstrar",
-  "status.media_hidden": "Medios celate",
+  "status.media_hidden": "Contento multimedial celate",
   "status.mention": "Mentionar @{name}",
   "status.more": "Plus",
   "status.mute": "Silentiar @{name}",
@@ -894,8 +902,6 @@
   "video.expand": "Expander video",
   "video.fullscreen": "Schermo plen",
   "video.hide": "Celar video",
-  "video.mute": "Silentiar le sono",
   "video.pause": "Pausa",
-  "video.play": "Reproducer",
-  "video.unmute": "Non plus silentiar le sono"
+  "video.play": "Reproducer"
 }
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index beb060eb79..102e547d40 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -29,7 +29,6 @@
   "account.endorse": "Tampilkan di profil",
   "account.featured_tags.last_status_at": "Kiriman terakhir pada {date}",
   "account.featured_tags.last_status_never": "Tidak ada kiriman",
-  "account.featured_tags.title": "Tagar {name} yang difiturkan",
   "account.follow": "Ikuti",
   "account.follow_back": "Ikuti balik",
   "account.followers": "Pengikut",
@@ -610,8 +609,6 @@
   "video.expand": "Perbesar video",
   "video.fullscreen": "Layar penuh",
   "video.hide": "Sembunyikan video",
-  "video.mute": "Bisukan suara",
   "video.pause": "Jeda",
-  "video.play": "Putar",
-  "video.unmute": "Bunyikan suara"
+  "video.play": "Putar"
 }
diff --git a/app/javascript/mastodon/locales/ie.json b/app/javascript/mastodon/locales/ie.json
index b04c951de6..7cd463727f 100644
--- a/app/javascript/mastodon/locales/ie.json
+++ b/app/javascript/mastodon/locales/ie.json
@@ -28,7 +28,6 @@
   "account.endorse": "Recomandar sur profil",
   "account.featured_tags.last_status_at": "Ultim posta ye {date}",
   "account.featured_tags.last_status_never": "Null postas",
-  "account.featured_tags.title": "Recomandat hashtags de {name}",
   "account.follow": "Sequer",
   "account.follow_back": "Sequer reciprocmen",
   "account.followers": "Sequitores",
@@ -522,7 +521,6 @@
   "poll_button.remove_poll": "Remover balotation",
   "privacy.change": "Changear li privatie del posta",
   "privacy.direct.long": "Omnes mentionat in li posta",
-  "privacy.direct.short": "Specific persones",
   "privacy.private.long": "Solmen tui sequitores",
   "privacy.private.short": "Sequitores",
   "privacy.public.long": "Quicunc in e ex Mastodon",
@@ -706,8 +704,6 @@
   "video.expand": "Expander video",
   "video.fullscreen": "Plen-ecran",
   "video.hide": "Celar video",
-  "video.mute": "Silentiar li son",
   "video.pause": "Pausar",
-  "video.play": "Reproducter",
-  "video.unmute": "Dessilentiar li son"
+  "video.play": "Reproducter"
 }
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index c69c5b801a..596ca4c3fe 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -4,7 +4,7 @@
   "about.disclaimer": "Mastodon esas libera, publikfonta e komercmarko di Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Expliko nedisponebla",
   "about.domain_blocks.preamble": "Mastodon generale permisas on vidar kontenajo e interagar kun uzanti de irga altra servilo en fediverso. Existas eceptioni quo facesis che ca partikulara servilo.",
-  "about.domain_blocks.silenced.explanation": "On generale ne vidar profili e kontenajo de ca servilo, se on ne reale trovar o voluntale juntar per sequar.",
+  "about.domain_blocks.silenced.explanation": "On generale ne vidar profili e enhavajo de ca servilo, se on ne intence serchar o voleskar per sequar.",
   "about.domain_blocks.silenced.title": "Limitizita",
   "about.domain_blocks.suspended.explanation": "Nula informi de ca servili procedagesos o retenesos o interchanjesos, do irga interago o komuniko kun uzanti de ca servili esas neposibla.",
   "about.domain_blocks.suspended.title": "Restriktita",
@@ -12,7 +12,7 @@
   "about.powered_by": "Necentraligita sociala ret quo povigesas da {mastodon}",
   "about.rules": "Servilreguli",
   "account.account_note_header": "Personala noto",
-  "account.add_or_remove_from_list": "Insertez o removez de listi",
+  "account.add_or_remove_from_list": "Adjuntar o forigar de listi",
   "account.badges.bot": "Boto",
   "account.badges.group": "Grupo",
   "account.block": "Blokusar @{name}",
@@ -24,12 +24,11 @@
   "account.direct": "Private mencionez @{name}",
   "account.disable_notifications": "Cesez avizar me kande @{name} postas",
   "account.domain_blocked": "Domain hidden",
-  "account.edit_profile": "Modifikar profilo",
+  "account.edit_profile": "Redaktar profilo",
   "account.enable_notifications": "Avizez me kande @{name} postas",
   "account.endorse": "Traito di profilo",
   "account.featured_tags.last_status_at": "Antea posto ye {date}",
   "account.featured_tags.last_status_never": "Nula posti",
-  "account.featured_tags.title": "Estalita hashtagi di {name}",
   "account.follow": "Sequar",
   "account.follow_back": "Anke sequez",
   "account.followers": "Sequanti",
@@ -45,7 +44,7 @@
   "account.languages": "Chanjez abonita lingui",
   "account.link_verified_on": "Proprieteso di ca ligilo kontrolesis ye {date}",
   "account.locked_info": "La privatesostaco di ca konto fixesas quale lokata. Proprietato manue kontrolas personi qui povas sequar.",
-  "account.media": "Medio",
+  "account.media": "Audvidaji",
   "account.mention": "Mencionar @{name}",
   "account.moved_to": "{name} indikis ke lua nova konto es nune:",
   "account.mute": "Celar @{name}",
@@ -56,7 +55,7 @@
   "account.no_bio": "Deskriptajo ne provizesis.",
   "account.open_original_page": "Apertez originala pagino",
   "account.posts": "Mesaji",
-  "account.posts_with_replies": "Posti e respondi",
+  "account.posts_with_replies": "Afishi e respondi",
   "account.report": "Denuncar @{name}",
   "account.requested": "Vartante aprobo",
   "account.requested_follow": "{name} demandis sequar tu",
@@ -86,12 +85,38 @@
   "alert.unexpected.message": "Neexpektita eroro eventis.",
   "alert.unexpected.title": "Problemo!",
   "alt_text_badge.title": "Alternativa texto",
+  "alt_text_modal.add_alt_text": "Adjuntar alternativa texto",
+  "alt_text_modal.add_text_from_image": "Adjuntar texto de imajo",
+  "alt_text_modal.cancel": "Nuligar",
+  "alt_text_modal.change_thumbnail": "Chanjar imajeto",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Priskribar co por personi kun auddeskapableso…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Priskribar co por personi kun viddeskapableso…",
+  "alt_text_modal.done": "Finis",
   "announcement.announcement": "Anunco",
+  "annual_report.summary.archetype.booster": "La plurrepetanto",
+  "annual_report.summary.archetype.lurker": "La plurcelanto",
+  "annual_report.summary.archetype.oracle": "La pluraktivo",
+  "annual_report.summary.archetype.pollster": "La votinquestoiganto",
+  "annual_report.summary.archetype.replier": "La plurrespondanto",
+  "annual_report.summary.followers.followers": "sequanti",
+  "annual_report.summary.followers.total": "{count} sumo",
+  "annual_report.summary.here_it_is": "Caibe es vua rivido ye {year}:",
+  "annual_report.summary.highlighted_post.by_favourites": "maxim prizita mesajo",
+  "annual_report.summary.highlighted_post.by_reblogs": "maxim repetita mesajo",
+  "annual_report.summary.highlighted_post.by_replies": "mesajo kun la maxim multa respondi",
+  "annual_report.summary.highlighted_post.possessive": "di {name}",
+  "annual_report.summary.most_used_app.most_used_app": "maxim uzita aplikajo",
+  "annual_report.summary.most_used_hashtag.most_used_hashtag": "maxim uzita gretvorto",
+  "annual_report.summary.most_used_hashtag.none": "Nulo",
+  "annual_report.summary.new_posts.new_posts": "nova afishi",
+  "annual_report.summary.percentile.text": "<topLabel>To pozas vu sur la supro </topLabel><percentage></percentage><bottomLabel>di uzanti di {domain}.</bottomLabel>",
+  "annual_report.summary.percentile.we_wont_tell_bernie": "Ni ne dicas ad Bernio.",
+  "annual_report.summary.thanks": "Danki por partoprenar sur Mastodon!",
   "attachments_list.unprocessed": "(neprocedita)",
   "audio.hide": "Celez audio",
   "block_modal.remote_users_caveat": "Ni questionos {domain} di la servilo por respektar vua decido. Publika posti forsan ankore estas videbla a neenirinta uzanti.",
   "block_modal.show_less": "Montrar mine",
-  "block_modal.show_more": "Montrar plue",
+  "block_modal.show_more": "Montrar plu",
   "block_modal.they_cant_mention": "Oli ne povas mencionar o sequar vu.",
   "block_modal.they_cant_see_posts": "Oli ne povas vidar vua mesaji e vu ne vidos vidar olia.",
   "block_modal.they_will_know": "Oli povas vidar ke oli esas blokusita.",
@@ -110,6 +135,7 @@
   "bundle_column_error.routing.body": "Demandita pagino ne povas trovesar. Ka vu certe ke URL en situobuxo esar korekta?",
   "bundle_column_error.routing.title": "Eroro di 404",
   "bundle_modal_error.close": "Klozez",
+  "bundle_modal_error.message": "Ulo nefuncionas dum chargar ca skreno.",
   "bundle_modal_error.retry": "Probez itere",
   "closed_registrations.other_server_instructions": "Nam Mastodon es descentraligita, on povas krear konto che altra servilo e senegarde interagar kun ca servilo.",
   "closed_registrations_modal.description": "Nune on ne povas krear konto che {domain}, ma voluntez savar ke on ne bezonas konto specifike che {domain} por uzar Mastodon.",
@@ -118,19 +144,22 @@
   "closed_registrations_modal.title": "Krear konto che Mastodon",
   "column.about": "Pri co",
   "column.blocks": "Blokusita uzeri",
-  "column.bookmarks": "Libromarki",
+  "column.bookmarks": "Lektosigni",
   "column.community": "Lokala tempolineo",
+  "column.create_list": "Krear listo",
   "column.direct": "Privata mencioni",
   "column.directory": "Videz profili",
-  "column.domain_blocks": "Hidden domains",
+  "column.domain_blocks": "Blokusita domeni",
+  "column.edit_list": "Redaktar listo",
   "column.favourites": "Favoriziti",
   "column.firehose": "Nuna flui",
   "column.follow_requests": "Demandi di sequado",
   "column.home": "Hemo",
+  "column.list_members": "Administrar listomembri",
   "column.lists": "Listi",
   "column.mutes": "Celita uzeri",
   "column.notifications": "Savigi",
-  "column.pins": "Pinned toot",
+  "column.pins": "Adpinglita afishi",
   "column.public": "Federata tempolineo",
   "column_back_button.label": "Retro",
   "column_header.hide_settings": "Celez ajusti",
@@ -139,26 +168,28 @@
   "column_header.pin": "Pinglagez",
   "column_header.show_settings": "Montrez ajusti",
   "column_header.unpin": "Depinglagez",
+  "column_search.cancel": "Nuligar",
   "column_subheading.settings": "Ajusti",
   "community.column_settings.local_only": "Lokala nur",
-  "community.column_settings.media_only": "Media only",
+  "community.column_settings.media_only": "Nur audvidaji",
   "community.column_settings.remote_only": "Fora nur",
   "compose.language.change": "Chanjez linguo",
   "compose.language.search": "Trovez linguo...",
   "compose.published.body": "Posto publikigita.",
   "compose.published.open": "Apertez",
   "compose.saved.body": "Posto konservita.",
-  "compose_form.direct_message_warning_learn_more": "Lernez pluse",
+  "compose_form.direct_message_warning_learn_more": "Lernez plu",
   "compose_form.encryption_warning": "Posti en Mastodon ne intersequante chifrigesas. Ne partigez irga privata informo che Mastodon.",
-  "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
+  "compose_form.hashtag_warning": "Ca afisho ne listigesos kun irga gretvorto pro ke ol ne es publika.",
   "compose_form.lock_disclaimer": "Vua konto ne esas {locked}. Irgu povas sequar vu por vidar vua sequanto-nura posti.",
   "compose_form.lock_disclaimer.lock": "klefagesas",
-  "compose_form.placeholder": "Quo esas en tua spirito?",
-  "compose_form.poll.duration": "Votpostoduro",
+  "compose_form.placeholder": "Quon vu pensas?",
+  "compose_form.poll.duration": "Votinquestoduro",
   "compose_form.poll.multiple": "Multopla selekteso",
   "compose_form.poll.option_placeholder": "Selektato {number}",
-  "compose_form.poll.switch_to_multiple": "Chanjez votposto por permisar multiselektaji",
-  "compose_form.poll.switch_to_single": "Chanjez votposto por permisar una selektajo",
+  "compose_form.poll.single": "Unopla selektato",
+  "compose_form.poll.switch_to_multiple": "Chanjar votinquesto por permisar multopla selektati",
+  "compose_form.poll.switch_to_single": "Chanjez votinquesto por permisar unopla selektato",
   "compose_form.poll.type": "Stilo",
   "compose_form.publish": "Posto",
   "compose_form.publish_form": "Publish",
@@ -175,14 +206,21 @@
   "confirmations.delete_list.confirm": "Efacez",
   "confirmations.delete_list.message": "Ka vu certe volas netempale efacar ca listo?",
   "confirmations.delete_list.title": "Ka efacar listo?",
-  "confirmations.discard_edit_media.confirm": "Efacez",
-  "confirmations.discard_edit_media.message": "Vu havas nesparita chanji di mediodeskript o prevido, vu volas jus efacar?",
+  "confirmations.discard_edit_media.confirm": "Forigar",
+  "confirmations.discard_edit_media.message": "Vu havas nekonservita chanji di audvidajpriskribo o prevido, ka forigas ili irgakaze?",
   "confirmations.edit.confirm": "Modifikez",
   "confirmations.edit.message": "Modifikar nun remplasos la mesajo quon vu nune skribas. Ka vu certe volas procedar?",
   "confirmations.edit.title": "Ka remplasar posto?",
+  "confirmations.follow_to_list.confirm": "Sequar e adjuntar ad listo",
+  "confirmations.follow_to_list.message": "Vu bezonas sequar {name} por adjuntar lu ad listo.",
+  "confirmations.follow_to_list.title": "Ka sequar uzanto?",
   "confirmations.logout.confirm": "Ekirez",
   "confirmations.logout.message": "Ka tu certe volas ekirar?",
   "confirmations.logout.title": "Ka ekirar?",
+  "confirmations.missing_alt_text.confirm": "Adjuntar alternativa texto",
+  "confirmations.missing_alt_text.message": "Vua afisho enhavas audvidaji sen alternativa texto.",
+  "confirmations.missing_alt_text.secondary": "Mesajar irgamaniere",
+  "confirmations.missing_alt_text.title": "Ka adjuntar alternativa texto?",
   "confirmations.mute.confirm": "Silencigez",
   "confirmations.redraft.confirm": "Efacez e riskisez",
   "confirmations.redraft.message": "Ka vu certe volas efacar ca posto e riskisigar ol? Favoriziti e repeti esos perdita, e respondi al posto originala esos orfanigita.",
@@ -195,6 +233,7 @@
   "confirmations.unfollow.title": "Ka dessequar uzanto?",
   "content_warning.hide": "Celez posto",
   "content_warning.show": "Montrez nur",
+  "content_warning.show_more": "Montrar plu",
   "conversation.delete": "Efacez konverso",
   "conversation.mark_as_read": "Markizez quale lektita",
   "conversation.open": "Videz konverso",
@@ -210,6 +249,10 @@
   "disabled_account_banner.text": "Vua konto {disabledAccount} es nune desaktivigita.",
   "dismissable_banner.community_timeline": "Co esas maxim recenta publika posti de personi quo havas konto quo hostigesas da {domain}.",
   "dismissable_banner.dismiss": "Ignorez",
+  "dismissable_banner.explore_links": "Ca novaji es maxim partigita sur fediverso hodie.",
+  "dismissable_banner.explore_statuses": "Ca afishi di tota fediverso populareskis hodie.",
+  "dismissable_banner.explore_tags": "Ca gretvorti populareskas sur fediverso hodie.",
+  "dismissable_banner.public_timeline": "Co es la maxim lastatempa publika afishi di personi sur la fediverso qua personi di {domain} sequas.",
   "domain_block_modal.block": "Blokusez servilo",
   "domain_block_modal.block_account_instead": "Blokusez @{name} vice",
   "domain_block_modal.they_can_interact_with_old_posts": "Personi de ca servilo povas interagar kun vua desnova posti.",
@@ -239,7 +282,7 @@
   "emoji_button.custom": "Kustumizato",
   "emoji_button.flags": "Flagi",
   "emoji_button.food": "Manjajo & Drinkajo",
-  "emoji_button.label": "Insertar emoji",
+  "emoji_button.label": "Enpozar emocimajo",
   "emoji_button.nature": "Naturo",
   "emoji_button.not_found": "Nula tala parigata emojii",
   "emoji_button.objects": "Kozi",
@@ -257,14 +300,14 @@
   "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
   "empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!",
   "empty_column.direct": "Vu ankore ne havas irga direta mesaji. Kande vu sendos o recevos un, ol montresos hike.",
-  "empty_column.domain_blocks": "There are no hidden domains yet.",
-  "empty_column.explore_statuses": "Nulo esas tendenca nun. Videz itere pose!",
+  "empty_column.domain_blocks": "Ne havas blokusita domeni ankore.",
+  "empty_column.explore_statuses": "Nulo populareskas nun.",
   "empty_column.favourited_statuses": "Vu ankore ne havas irga posti favorizita. Kande vu favorizos un, ol montresos hike.",
   "empty_column.favourites": "Nulu favorizis ca posto. Kande ulu favorizis ol, lu montresos hike.",
   "empty_column.follow_requests": "Vu ne havas irga sequodemandi til nun. Kande vu ganas talo, ol montresos hike.",
   "empty_column.followed_tags": "Vu ankore ne sequis irga hashtago. Kande vu sequos un, ol montresos hike.",
   "empty_column.hashtag": "Esas ankore nulo en ta gretovorto.",
-  "empty_column.home": "Vua hemtempolineo esas vakua! Sequez plu multa personi por plenigar lu. {suggestions}",
+  "empty_column.home": "Vua hemtempolineo esas desplena!",
   "empty_column.list": "There is nothing in this list yet.",
   "empty_column.mutes": "Vu ne silencigis irga uzanti til nun.",
   "empty_column.notification_requests": "Finis. Kande vu recevas nova savigi, oli aparos hike segun vua preferaji.",
@@ -273,7 +316,7 @@
   "error.unexpected_crash.explanation": "Pro eroro en nia kodexo o vidilkonciliebloproblemo, ca pagino ne povas korekte montresar.",
   "error.unexpected_crash.explanation_addons": "Ca pagino ne povas korekte montresar. Ca eroro posible kauzigesas vidilplusajo o automata tradukutensili.",
   "error.unexpected_crash.next_steps": "Probez rifreshar pagino. Se to ne helpas, vu forsan ankore povas uzar Mastodon per diferenta vidilo o provizita softwaro.",
-  "error.unexpected_crash.next_steps_addons": "Probez desaktivigar e rifreshar pagino. Se to ne helpas, vu forsan ankore povas uzar Mastodon per diferenta vidilo o provizita softwaro.",
+  "error.unexpected_crash.next_steps_addons": "Probez desebligar ili e rifreshar la pagino.",
   "errors.unexpected_crash.copy_stacktrace": "Kopiez amastraso a klipplanko",
   "errors.unexpected_crash.report_issue": "Reportigez problemo",
   "explore.suggested_follows": "Personi",
@@ -281,13 +324,13 @@
   "explore.trending_links": "Novaji",
   "explore.trending_statuses": "Posti",
   "explore.trending_tags": "Hashtagi",
-  "filter_modal.added.context_mismatch_explanation": "Ca filtrilgrupo ne relatesas kun informo de ca acesesita posto. Se vu volas posto filtresar kun ca informo anke, vu bezonas modifikar filtrilo.",
+  "filter_modal.added.context_mismatch_explanation": "Ca filtrilgrupo ne uzesis ad informo di ca adirita afisho.",
   "filter_modal.added.context_mismatch_title": "Kontenajneparigeso!",
   "filter_modal.added.expired_explanation": "Ca filtrilgrupo expiris, vu bezonas chanjar expirtempo por apliko.",
   "filter_modal.added.expired_title": "Expirinta filtrilo!",
-  "filter_modal.added.review_and_configure": "Por kontrolar e plue ajustar ca filtrilgrupo, irez a {settings_link}.",
-  "filter_modal.added.review_and_configure_title": "Filtrilopcioni",
-  "filter_modal.added.settings_link": "opcionpagino",
+  "filter_modal.added.review_and_configure": "Por kontrolar e plu ajustar ca filtrilgrupo, irez a {settings_link}.",
+  "filter_modal.added.review_and_configure_title": "Filtrilpreferaji",
+  "filter_modal.added.settings_link": "preferajpagino",
   "filter_modal.added.short_explanation": "Ca posto adjuntesas a ca filtrilgrupo: {title}.",
   "filter_modal.added.title": "Filtrilo adjuntesas!",
   "filter_modal.select_filter.context_mismatch": "ne relatesas kun ca informo",
@@ -297,6 +340,7 @@
   "filter_modal.select_filter.subtitle": "Usez disponebla grupo o kreez novajo",
   "filter_modal.select_filter.title": "Filtragez ca posto",
   "filter_modal.title.status": "Filtragez posto",
+  "filter_warning.matches_filter": "Samas kam filtrilo \"<span>{title}</span>\"",
   "filtered_notifications_banner.pending_requests": "De {count, plural,=0 {nulu} one {1 persono} other {# personi}} quan vu forsan konocas",
   "filtered_notifications_banner.title": "Filtrilita savigi",
   "firehose.all": "Omno",
@@ -306,7 +350,7 @@
   "follow_request.reject": "Refuzar",
   "follow_requests.unlocked_explanation": "Quankam vua konto ne klefklozesis, la {domain} laborero pensas ke vu forsan volas kontralar sequodemandi de ca konti manuale.",
   "follow_suggestions.curated_suggestion": "Selektato de jeranto",
-  "follow_suggestions.dismiss": "Ne montrez pluse",
+  "follow_suggestions.dismiss": "Ne montrez denove",
   "follow_suggestions.featured_longer": "Selektesis da la grupo di {domain}",
   "follow_suggestions.friends_of_friends_longer": "Populara inter personi quan vu sequas",
   "follow_suggestions.hints.featured": "Ca profilo selektesis da la grupo di {domain}.",
@@ -328,13 +372,15 @@
   "footer.privacy_policy": "Guidilo pri privateso",
   "footer.source_code": "Vidar la fontokodexo",
   "footer.status": "Stando",
+  "footer.terms_of_service": "Serveskondicioni",
   "generic.saved": "Sparesis",
   "getting_started.heading": "Debuto",
+  "hashtag.admin_moderation": "Desklozar administrointervizajo por #{name}",
   "hashtag.column_header.tag_mode.all": "e {additional}",
   "hashtag.column_header.tag_mode.any": "o {additional}",
   "hashtag.column_header.tag_mode.none": "sen {additional}",
   "hashtag.column_settings.select.no_options_message": "Nula sugestati",
-  "hashtag.column_settings.select.placeholder": "Insertez hashtagi…",
+  "hashtag.column_settings.select.placeholder": "Insertez gretvorti…",
   "hashtag.column_settings.tag_mode.all": "Omna co",
   "hashtag.column_settings.tag_mode.any": "Irga co",
   "hashtag.column_settings.tag_mode.none": "Nula co",
@@ -342,8 +388,8 @@
   "hashtag.counter_by_accounts": "{count, plural, one {{counter} partoprenanto} other {{counter} partoprenanti}}",
   "hashtag.counter_by_uses": "{count, plural, one {{counter} posto} other {{counter} posti}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} posto} other {{counter} posti}} hodie",
-  "hashtag.follow": "Sequez hashtago",
-  "hashtag.unfollow": "Desequez hashtago",
+  "hashtag.follow": "Sequar gretvorto",
+  "hashtag.unfollow": "Dessequar gretvorto",
   "hashtags.and_other": "…e {count, plural, one {# plusa}other {# plusa}}",
   "hints.profiles.followers_may_be_missing": "Sequanti di ca profilo forsan ne esas hike.",
   "hints.profiles.follows_may_be_missing": "Sequati di ca profilo forsan ne esas hike.",
@@ -371,12 +417,23 @@
   "ignore_notifications_modal.not_followers_title": "Ka ignorar savigi de personi qua ne sequas vu?",
   "ignore_notifications_modal.not_following_title": "Ka ignorar savigi de personi quan vu ne sequas?",
   "ignore_notifications_modal.private_mentions_title": "Ka ignorar savigi de nekonocita privata mencionii?",
+  "info_button.label": "Helpo",
+  "info_button.what_is_alt_text": "<h1>Quo alternativa texto es?</h1> <p>On povas bonigar adireblo e kompreno por omno per skribar klara, deslonga e fakta alternative texto.</p>",
+  "interaction_modal.action.favourite": "Por durar, vu bezonas prizar de vua konto.",
+  "interaction_modal.action.follow": "Por durar, vu bezonas sequar de vua konto.",
+  "interaction_modal.action.reblog": "Por durar, vu bezonas riblogar de vua konto.",
+  "interaction_modal.action.reply": "Por durar, vu bezonas respondar de vua konto.",
+  "interaction_modal.action.vote": "Por durar, vu bezonas vocdonar de vua konto.",
+  "interaction_modal.go": "Irar",
+  "interaction_modal.no_account_yet": "Ka vu ne havas konto ankore?",
   "interaction_modal.on_another_server": "Che diferanta servilo",
   "interaction_modal.on_this_server": "Che ca servilo",
   "interaction_modal.title.favourite": "Favorizez ca posto da {name}",
   "interaction_modal.title.follow": "Sequez {name}",
   "interaction_modal.title.reblog": "Repetez posto di {name}",
   "interaction_modal.title.reply": "Respondez posto di {name}",
+  "interaction_modal.title.vote": "Votar sur votinquesto di {name}",
+  "interaction_modal.username_prompt": "Exemple {example}",
   "intervals.full.days": "{number, plural, one {# dio} other {# dii}}",
   "intervals.full.hours": "{number, plural, one {# horo} other {# hori}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}",
@@ -401,8 +458,8 @@
   "keyboard_shortcuts.muted": "to open muted users list",
   "keyboard_shortcuts.my_profile": "to open your profile",
   "keyboard_shortcuts.notifications": "to open notifications column",
-  "keyboard_shortcuts.open_media": "to open media",
-  "keyboard_shortcuts.pinned": "to open pinned toots list",
+  "keyboard_shortcuts.open_media": "Desklozar audvidaji",
+  "keyboard_shortcuts.pinned": "Desklozar listo di adpinglita afishi",
   "keyboard_shortcuts.profile": "to open author's profile",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.requests": "to open follow requests list",
@@ -410,8 +467,9 @@
   "keyboard_shortcuts.spoilers": "to show/hide CW field",
   "keyboard_shortcuts.start": "to open \"get started\" column",
   "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
-  "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
+  "keyboard_shortcuts.toggle_sensitivity": "Montrar/celar audvidaji",
   "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.translate": "por tradukar mesajo",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Klozar",
@@ -424,14 +482,35 @@
   "link_preview.author": "Da {name}",
   "link_preview.more_from_author": "Plua de {name}",
   "link_preview.shares": "{count, plural,one {{counter} posto} other {{counter} posti}}",
+  "lists.add_member": "Adjuntar",
+  "lists.add_to_list": "Adjuntar ad listo",
+  "lists.add_to_lists": "Adjuntar {name} ad listi",
+  "lists.create": "Krear",
+  "lists.create_a_list_to_organize": "Krear nova listo por organizar vua hemfluo",
+  "lists.create_list": "Krear listo",
   "lists.delete": "Efacez listo",
-  "lists.edit": "Modifikez listo",
+  "lists.done": "Finis",
+  "lists.edit": "Redaktar listo",
+  "lists.exclusive": "Celar membri sur Hemo",
+  "lists.exclusive_hint": "Se ulu es en ca listo, celez lu sur vua hemfluo por evitar vidar lua afishi denove.",
+  "lists.find_users_to_add": "Serchi uzanti por adjuntar",
+  "lists.list_members": "Listigar membri",
+  "lists.list_members_count": "{count, plural,one {# membro} other {#membri}}",
+  "lists.list_name": "Listonomo",
+  "lists.new_list_name": "Nova listonomo",
+  "lists.no_lists_yet": "Nula listi ankore.",
+  "lists.no_members_yet": "Nula membri ankore.",
+  "lists.no_results_found": "Nula rezulto trovigesis.",
+  "lists.remove_member": "Forigar",
   "lists.replies_policy.followed": "Irga sequita uzanto",
   "lists.replies_policy.list": "Membro di listo",
   "lists.replies_policy.none": "Nulu",
+  "lists.save": "Konservar",
+  "lists.search": "Serchar",
+  "lists.show_replies_to": "Inkluzar respondi de listomembri",
   "load_pending": "{count, plural, one {# nova kozo} other {# nova kozi}}",
   "loading_indicator.label": "Kargante…",
-  "media_gallery.hide": "Celez",
+  "media_gallery.hide": "Celar",
   "moved_to_account_banner.text": "Vua konto {disabledAccount} es nune desaktiva pro ke vu movis a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Celez de savigi",
   "mute_modal.hide_options": "Celez preferaji",
@@ -446,12 +525,12 @@
   "navigation_bar.administration": "Administro",
   "navigation_bar.advanced_interface": "Apertez per retintervizajo",
   "navigation_bar.blocks": "Blokusita uzeri",
-  "navigation_bar.bookmarks": "Libromarki",
+  "navigation_bar.bookmarks": "Lektosigni",
   "navigation_bar.community_timeline": "Lokala tempolineo",
   "navigation_bar.compose": "Compose new toot",
   "navigation_bar.direct": "Privata mencioni",
   "navigation_bar.discover": "Deskovrez",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.domain_blocks": "Blokusita domeni",
   "navigation_bar.explore": "Explorez",
   "navigation_bar.favourites": "Favoriziti",
   "navigation_bar.filters": "Silencigita vorti",
@@ -464,7 +543,7 @@
   "navigation_bar.mutes": "Celita uzeri",
   "navigation_bar.opened_in_classic_interface": "Posti, konti e altra pagini specifika apertesas en la retovidilo klasika.",
   "navigation_bar.personal": "Personala",
-  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.pins": "Adpinglita afishi",
   "navigation_bar.preferences": "Preferi",
   "navigation_bar.public_timeline": "Federata tempolineo",
   "navigation_bar.search": "Serchez",
@@ -477,9 +556,14 @@
   "notification.admin.report_statuses_other": "{name} raportis {target}",
   "notification.admin.sign_up": "{name} registresis",
   "notification.admin.sign_up.name_and_others": "{name} e {count, plural,one {# altru} other {#altri}} enrejistris",
+  "notification.annual_report.message": "Yen vua ye {year} #Wrapstodon!",
+  "notification.annual_report.view": "Vidar #Wrapstodon",
   "notification.favourite": "{name} favorizis tua mesajo",
-  "notification.favourite.name_and_others_with_link": "{name} e <a>{count, plural,one {# altru} other {# altri}}</a> favorizis vua posto",
+  "notification.favourite.name_and_others_with_link": "{name} e <a>{count, plural,one {# altru} other {# altri}}</a> stelumis vua afisho",
+  "notification.favourite_pm": "{name} prizis vua privata menciono",
+  "notification.favourite_pm.name_and_others_with_link": "{name} e <a>{count, plural,one {# altro} other {# altri}}</a> prizis vua privata menciono",
   "notification.follow": "{name} sequeskis tu",
+  "notification.follow.name_and_others": "{name} e <a>{count, plural,one {# altro} other {# altri}}</a> sequis vu",
   "notification.follow_request": "{name} demandas sequar vu",
   "notification.follow_request.name_and_others": "{name} e {count, plural,one {# altru} other {# altri}} volas sequar vu",
   "notification.label.mention": "Mencionez",
@@ -487,37 +571,38 @@
   "notification.label.private_reply": "Privata respondo",
   "notification.label.reply": "Respondez",
   "notification.mention": "Mencionez",
-  "notification.moderation-warning.learn_more": "Lernez pluse",
+  "notification.mentioned_you": "{name} mencionis vu",
+  "notification.moderation-warning.learn_more": "Lernez plu",
   "notification.moderation_warning": "Vu recevis jeraverto",
   "notification.moderation_warning.action_delete_statuses": "Kelka vua posti efacesis.",
-  "notification.moderation_warning.action_disable": "Vua konto estas desaktivigita.",
+  "notification.moderation_warning.action_disable": "Vua konto es desaktivigita.",
   "notification.moderation_warning.action_mark_statuses_as_sensitive": "Kelka vua posti markizesis quale sentoza.",
   "notification.moderation_warning.action_none": "Vua konto recevis jeraverto.",
   "notification.moderation_warning.action_sensitive": "Vua posti markizesos quale sentoza pos nun.",
   "notification.moderation_warning.action_silence": "Vua konto limitizesis.",
   "notification.moderation_warning.action_suspend": "Vua konto restriktesis.",
-  "notification.own_poll": "Vua votposto finigis",
-  "notification.poll": "Votposto quan vu partoprenis finis",
+  "notification.own_poll": "Vua votinquesto fineskis",
+  "notification.poll": "Votinquesto ube vu votis fineskis",
   "notification.reblog": "{name} repetis tua mesajo",
   "notification.reblog.name_and_others_with_link": "{name} e <a>{count, plural,one {# altru} other {#altri}}</a> repetis vua posto",
   "notification.relationships_severance_event": "Desganis konekteso kun {name}",
   "notification.relationships_severance_event.account_suspension": "Administranto de {from} restriktis {target}, do vu ne povas plue recevar novaji de lu o interagar kun lu.",
   "notification.relationships_severance_event.domain_block": "Administranto de {from} blokusis {target}, e anke {followersCount} de vua sequanti e {followingCount, plural, one {# konto} other {# konti}} quan vu sequas.",
-  "notification.relationships_severance_event.learn_more": "Lernez pluse",
+  "notification.relationships_severance_event.learn_more": "Lernez plu",
   "notification.relationships_severance_event.user_domain_block": "Vu blokusis {target}, do efacis {followersCount} de vua sequanti e {followingCount, plural, one {# konto} other {#konti}} quan vu sequis.",
   "notification.status": "{name} nove postigis",
-  "notification.update": "{name} modifikis posto",
+  "notification.update": "{name} redaktis afisho",
   "notification_requests.accept": "Aceptez",
   "notification_requests.accept_multiple": "{count, plural, one {Aceptar # demando…} other {Aceptar # demandi…}}",
   "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar demando} other {Aceptar demandi}}",
   "notification_requests.confirm_accept_multiple.message": "Vu aceptos {count, plural, one {1 savigdemando} other {# savigdemandi}}. Ka vu certe volas durar?",
   "notification_requests.confirm_accept_multiple.title": "Ka aceptar savigdemandi?",
   "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Ignorez demando} other {Ignorez demandi}}",
-  "notification_requests.confirm_dismiss_multiple.message": "Vu ignoros {count, plural, one {1 savigdemando} other {# savigdemandi}}. Vu ne povas facile ganar {count, plural, one {ol} other {oli}} pluse. Ka vu esas certe ke vu volas durar?",
+  "notification_requests.confirm_dismiss_multiple.message": "Vu ignoros {count, plural, one {1 savigdemando} other {# savigdemandi}}. Vu ne povas facile ganar {count, plural, one {ol} other {oli}} denove. Ka vu esas certe ke vu volas durar?",
   "notification_requests.confirm_dismiss_multiple.title": "Ka ignorar savigdemandi?",
   "notification_requests.dismiss": "Ignorez",
   "notification_requests.dismiss_multiple": "{count, plural,one {Ignorez # demando…} other {Ignorez # demandi…}}",
-  "notification_requests.edit_selection": "Modifikez",
+  "notification_requests.edit_selection": "Redaktar",
   "notification_requests.exit_selection": "Finas",
   "notification_requests.explainer_for_limited_account": "Savigi de ca konto filtresis pro ke la konto limitizesis da jeranto.",
   "notification_requests.explainer_for_limited_remote_account": "Savigi de ca konto filtresis pro ke la konto o olua servilo limitizesis da jeranto.",
@@ -537,8 +622,9 @@
   "notifications.column_settings.filter_bar.category": "Rapidfiltrilbaro",
   "notifications.column_settings.follow": "Nova sequanti:",
   "notifications.column_settings.follow_request": "Nova sequodemandi:",
+  "notifications.column_settings.group": "Grupo",
   "notifications.column_settings.mention": "Mencioni:",
-  "notifications.column_settings.poll": "Votpostorezulti:",
+  "notifications.column_settings.poll": "Votinquestorezulti:",
   "notifications.column_settings.push": "Pulsavizi",
   "notifications.column_settings.reblog": "Repeti:",
   "notifications.column_settings.show": "Montrar en kolumno",
@@ -546,19 +632,19 @@
   "notifications.column_settings.status": "New toots:",
   "notifications.column_settings.unread_notifications.category": "Nelektita avizi",
   "notifications.column_settings.unread_notifications.highlight": "Briligez nelektita avizi",
-  "notifications.column_settings.update": "Modifikati:",
+  "notifications.column_settings.update": "Redaktati:",
   "notifications.filter.all": "Omna",
   "notifications.filter.boosts": "Repeti",
   "notifications.filter.favourites": "Favoriziti",
   "notifications.filter.follows": "Sequati",
   "notifications.filter.mentions": "Mencioni",
-  "notifications.filter.polls": "Votpostorezulti",
+  "notifications.filter.polls": "Votinquestorezulti",
   "notifications.filter.statuses": "Novaji de personi quon vu sequas",
   "notifications.grant_permission": "Donez permiso.",
   "notifications.group": "{count} avizi",
   "notifications.mark_as_read": "Markizez singla avizi quale lektita",
   "notifications.permission_denied": "Desktopavizi esas nedisplonebla pro antea refuzita vidilpermisdemando",
-  "notifications.permission_denied_alert": "Desktopavizi ne povas aktivigesar pro ke vidilpermiso refuzesis",
+  "notifications.permission_denied_alert": "Komputilsavigi ne povas ebligesar, pro ke retumilpermiso desaceptesis antee",
   "notifications.permission_required": "Desktopavizi esas nedisplonebla pro ke bezonata permiso ne donesis.",
   "notifications.policy.accept": "Aceptez",
   "notifications.policy.accept_hint": "Montrez en savigi",
@@ -577,10 +663,14 @@
   "notifications.policy.filter_private_mentions_hint": "Filtrita se ol ne esas respondo a vua sua menciono o se vu sequas la sendanto",
   "notifications.policy.filter_private_mentions_title": "Nekonocita privata mencioni",
   "notifications.policy.title": "Regular savigi de…",
-  "notifications_permission_banner.enable": "Aktivigez desktopavizi",
-  "notifications_permission_banner.how_to_control": "Por ganar avizi kande Mastodon ne esas apertita, aktivigez dekstopavizi. Vu povas precize regularar quale interakti facas deskstopavizi tra la supera {icon} butono pos oli aktivigesis.",
+  "notifications_permission_banner.enable": "Ebligar komputilsavigi",
+  "notifications_permission_banner.how_to_control": "Por ganar savigi kande Mastodon ne es desklozita, ebligez komputilsavigi.",
   "notifications_permission_banner.title": "Irga kozo ne pasas vu",
+  "onboarding.follows.back": "Retro",
+  "onboarding.follows.done": "Finis",
   "onboarding.follows.empty": "Regretinde, nula rezultajo povas montresar nune. Vu povas esforcar serchar, o irar al explorala pagino por trovar personi sequinda, o esforcar itere pose.",
+  "onboarding.follows.search": "Serchar",
+  "onboarding.follows.title": "Sequez personi por komencar",
   "onboarding.profile.discoverable": "Trovebligez mea profilo",
   "onboarding.profile.discoverable_hint": "Se vu selektas deskovrebleso che Mastodon, vua posti povas aparar en sercho-rezultaji e populari, e vua profilo forsan sugestesos a personi kun interesi simila a vua.",
   "onboarding.profile.display_name": "Publika nomo",
@@ -595,29 +685,31 @@
   "password_confirmation.mismatching": "La konfirmo dil pasvorto ne egalesas",
   "picture_in_picture.restore": "Retropozez",
   "poll.closed": "Klozita",
-  "poll.refresh": "Rifreshez",
+  "poll.refresh": "Rifreshar",
   "poll.reveal": "Vidar rezultaji",
   "poll.total_people": "{count, plural, one {# persono} other {# personi}}",
   "poll.total_votes": "{count, plural, one {# voto} other {# voti}}",
-  "poll.vote": "Votez",
+  "poll.vote": "Votar",
   "poll.voted": "Vu ja votis ca respondo",
   "poll.votes": "{votes, plural, one {# voto} other {# voti}}",
-  "poll_button.add_poll": "Insertez votposto",
-  "poll_button.remove_poll": "Efacez votposto",
+  "poll_button.add_poll": "Adjuntar votinquesto",
+  "poll_button.remove_poll": "Forigar votinquesto",
   "privacy.change": "Aranjar privateso di mesaji",
   "privacy.direct.long": "Omnu quan mencionesis en la posto",
-  "privacy.direct.short": "Specifika personi",
+  "privacy.direct.short": "Privata menciono",
   "privacy.private.long": "Nur vua sequanti",
   "privacy.private.short": "Sequanti",
   "privacy.public.long": "Ulu de e ne de Mastodon",
   "privacy.public.short": "Publike",
-  "privacy.unlisted.additional": "Co kondutas exakte kam publika, escepte la posto ne aparos en viva novajari o gretiketi, exploro, o sercho di Mastodon, mem se vu esas volunta totkonte.",
+  "privacy.unlisted.additional": "Co kondutas exakte kam publika, ecepte la posto ne aparos en nuna flui o gretvorti, exploro, o sercho di Mastodon, mem se vu esas volunta totkonte.",
   "privacy.unlisted.long": "Min multa algoritmoridikuli",
   "privacy.unlisted.short": "Deslauta publiko",
   "privacy_policy.last_updated": "Antea novajo ye {date}",
   "privacy_policy.title": "Privatesguidilo",
   "recommended": "Rekomendata",
   "refresh": "Rifreshez",
+  "regeneration_indicator.please_stand_by": "Vartez.",
+  "regeneration_indicator.preparing_your_home_feed": "Preparas vua hemfluo…",
   "relative_time.days": "{number}d",
   "relative_time.full.days": "{number, plural, one {# dio} other {# dii}} ante nun",
   "relative_time.full.hours": "{number, plural, one {# horo} other {# hori}} ante nun",
@@ -629,9 +721,9 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "relative_time.today": "hodie",
-  "reply_indicator.attachments": "{count, plural, one {# atachajo} other {# atachaji}}",
+  "reply_indicator.attachments": "{count, plural, one {# addonajo} other {# addonaji}}",
   "reply_indicator.cancel": "Nihiligar",
-  "reply_indicator.poll": "Votposto",
+  "reply_indicator.poll": "Votinquesto",
   "report.block": "Restriktez",
   "report.block_explanation": "Vu ne vidos olia posti. Oli ne povas vidar vua posti o sequar vu. Oli savos ke oli restriktesis.",
   "report.categories.legal": "Legala",
@@ -666,13 +758,13 @@
   "report.statuses.title": "Ka existas irga posti quo suportas ca raporto?",
   "report.submit": "Sendar",
   "report.target": "Denuncante",
-  "report.thanks.take_action": "Co esas vua opcioni por regularar quo vu vidas che Mastodon:",
+  "report.thanks.take_action": "Yen vua preferaji por regularar quon vu vidas sur Mastodon:",
   "report.thanks.take_action_actionable": "Dum ke ni kontrolas co, vu povas demarshar kontra @{name}:",
   "report.thanks.title": "Ka vu ne volas vidar co?",
   "report.thanks.title_actionable": "Danko por raportizar, ni kontrolos co.",
   "report.unfollow": "Desequez @{name}",
-  "report.unfollow_explanation": "Vu sequas ca konto. Por ne vidar olia posti en vua hemniuzeto pluse, desequez oli.",
-  "report_notification.attached_statuses": "{count, plural,one {{count} posti} other {{count} posti}} adjuntesas",
+  "report.unfollow_explanation": "Vu sequas ca konto.",
+  "report_notification.attached_statuses": "{count, plural,one {{count} posti} other {{count} posti}} addonita",
   "report_notification.categories.legal": "Legala",
   "report_notification.categories.legal_sentence": "deslegala kontenajo",
   "report_notification.categories.other": "Altra",
@@ -700,9 +792,12 @@
   "search_popout.user": "uzanto",
   "search_results.accounts": "Profili",
   "search_results.all": "Omna",
-  "search_results.hashtags": "Hashtagi",
+  "search_results.hashtags": "Gretvorti",
+  "search_results.no_results": "Nula rezulto.",
+  "search_results.no_search_yet": "Probez serchar afishi, profili o gretvorti.",
   "search_results.see_all": "Videz omni",
   "search_results.statuses": "Posti",
+  "search_results.title": "Serchar \"{q}\"",
   "server_banner.about_active_users": "Personi quo uzas ca servilo dum antea 30 dii (monate aktiva uzanti)",
   "server_banner.active_users": "aktiva uzanti",
   "server_banner.administered_by": "Administresis da:",
@@ -717,7 +812,7 @@
   "status.admin_domain": "Apertez jerintervizajo por {domain}",
   "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Restriktez @{name}",
-  "status.bookmark": "Libromarko",
+  "status.bookmark": "Lektosigno",
   "status.cancel_reblog_private": "Desrepetez",
   "status.cannot_reblog": "Ca posto ne povas repetesar",
   "status.continued_thread": "Durigita postaro",
@@ -726,34 +821,35 @@
   "status.detailed_status": "Detala konversvido",
   "status.direct": "Private mencionez @{name}",
   "status.direct_indicator": "Privata menciono",
-  "status.edit": "Modifikez",
-  "status.edited": "Recente modifikesis ye {date}",
-  "status.edited_x_times": "Modifikesis {count, plural, one {{count} foyo} other {{count} foyi}}",
+  "status.edit": "Redaktar",
+  "status.edited": "Lastatempe redaktesar ye {date}",
+  "status.edited_x_times": "Redaktesis ye {count, plural, one {{count} foyo} other {{count} foyi}}",
   "status.embed": "Ganez adherkodexo",
   "status.favourite": "Favorizar",
-  "status.favourites": "{count, plural, one {favorizo} other {favorizi}}",
+  "status.favourites": "{count, plural, one {stelumo} other {stelumi}}",
   "status.filter": "Filtragez ca posto",
   "status.history.created": "{name} kreis ye {date}",
-  "status.history.edited": "{name} modifikis ye {date}",
-  "status.load_more": "Kargar pluse",
+  "status.history.edited": "{name} redaktis ye {date}",
+  "status.load_more": "Kargar plu",
   "status.media.open": "Klikez por apertar",
   "status.media.show": "Klikez por montrar",
-  "status.media_hidden": "Kontenajo celita",
+  "status.media_hidden": "Audvidaji es celita",
   "status.mention": "Mencionar @{name}",
-  "status.more": "Pluse",
+  "status.more": "Plu",
   "status.mute": "Silencigez @{name}",
   "status.mute_conversation": "Silencigez konverso",
   "status.open": "Detaligar ca mesajo",
   "status.pin": "Pinglagez che profilo",
-  "status.pinned": "Pinned toot",
-  "status.read_more": "Lektez pluse",
+  "status.pinned": "Adpinglita afisho",
+  "status.read_more": "Lektez plu",
   "status.reblog": "Repetez",
   "status.reblog_private": "Repetez kun originala videbleso",
   "status.reblogged_by": "{name} repetis",
   "status.reblogs": "{count, plural, one {repeto} other {repeti}}",
   "status.reblogs.empty": "Nulu ja repetis ca posto. Kande ulu facas lo, lu montresos hike.",
   "status.redraft": "Efacez e riskisigez",
-  "status.remove_bookmark": "Efacez libromarko",
+  "status.remove_bookmark": "Forigar lektosigno",
+  "status.remove_favourite": "Forigar de priziti",
   "status.replied_in_thread": "Respondesis en postaro",
   "status.replied_to": "Respondis a {name}",
   "status.reply": "Respondar",
@@ -762,9 +858,9 @@
   "status.sensitive_warning": "Trubliva kontenajo",
   "status.share": "Partigez",
   "status.show_less_all": "Montrez min por omno",
-  "status.show_more_all": "Montrez pluse por omno",
+  "status.show_more_all": "Montrez plu por omno",
   "status.show_original": "Montrez originalo",
-  "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
+  "status.title.with_attachments": "{user} afishis {attachmentCount, plural, one {addonajo} other {{attachmentCount} addonaji}}",
   "status.translate": "Tradukez",
   "status.translated_from_with": "Tradukita de {lang} per {provider}",
   "status.uncached_media_warning": "Previdajo nedisponebla",
@@ -775,38 +871,37 @@
   "subscribed_languages.target": "Chanjez abonita lingui por {target}",
   "tabs_bar.home": "Hemo",
   "tabs_bar.notifications": "Savigi",
+  "terms_of_service.title": "Servtermini",
   "time_remaining.days": "{number, plural, one {# dio} other {# dii}} restas",
   "time_remaining.hours": "{number, plural, one {# horo} other {# hori}} restas",
   "time_remaining.minutes": "{number, plural, one {# minuto} other {# minuti}} restas",
   "time_remaining.moments": "Poka sekundi restas",
   "time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundi}} restas",
   "trends.counter_by_accounts": "{count, plural,one {{counter} persono} other {{counter} personi}} en antea {days, plural,one {dio} other {{days} dii}}",
-  "trends.trending_now": "Tendencigas nun",
+  "trends.trending_now": "Populareskas nun",
   "ui.beforeunload": "Vua skisato perdesos se vu ekiras Mastodon.",
   "units.short.billion": "{count}G",
   "units.short.million": "{count}M",
   "units.short.thousand": "{count}K",
-  "upload_area.title": "Tranar faligar por kargar",
-  "upload_button.label": "Adjuntar kontenajo",
-  "upload_error.limit": "Failadcharglimito ecesesis.",
-  "upload_error.poll": "Failadchargo ne permisesas kun votposti.",
-  "upload_form.drag_and_drop.instructions": "Por tenar mediatachajo, presez spaco o eniro. Presez spaco o eniro itere por destenar la mediatachajo en olua nova loko, o presez eskapo por anular.",
-  "upload_form.drag_and_drop.on_drag_cancel": "Tiro anulesis. Mediatachajo {item} destenesis.",
-  "upload_form.drag_and_drop.on_drag_end": "Mediatachajo {item} destenesis.",
-  "upload_form.drag_and_drop.on_drag_over": "Mediatachajo {item} movigesis.",
-  "upload_form.drag_and_drop.on_drag_start": "Tenis mediatachajo {item}.",
-  "upload_form.edit": "Modifikez",
-  "upload_progress.label": "Kargante...",
+  "upload_area.title": "Tenar e destenar por adkargar",
+  "upload_button.label": "Adjuntar imaji, video o sondosiero",
+  "upload_error.limit": "Dosieradkarglimito ecesesis.",
+  "upload_error.poll": "Dosieradkargo ne permisesas kun votinquesti.",
+  "upload_form.drag_and_drop.instructions": "Por tenar audvidajaddonajo, presez spaco o eniro. Presez spaco o eniro denove por destenar la audvidajatachajo en olua nova loko, o presez eskapo por nuligar.",
+  "upload_form.drag_and_drop.on_drag_cancel": "Tiro nuligesis.",
+  "upload_form.drag_and_drop.on_drag_end": "Audvidajaddonajo {item} destenesis.",
+  "upload_form.drag_and_drop.on_drag_over": "Audvidajaddonajo {item} movigesis.",
+  "upload_form.drag_and_drop.on_drag_start": "Tenis audvidajaddonajo {item}.",
+  "upload_form.edit": "Redaktar",
+  "upload_progress.label": "Adkargas...",
   "upload_progress.processing": "Traktante…",
   "username.taken": "Ta uzantnomo ja es posedita. Provez altro",
   "video.close": "Klozez video",
-  "video.download": "Deschargez failo",
+  "video.download": "Deschargar dosiero",
   "video.exit_fullscreen": "Ekirez plena skreno",
   "video.expand": "Expansez video",
   "video.fullscreen": "Plena skreno",
   "video.hide": "Celez video",
-  "video.mute": "Silencigez sono",
   "video.pause": "Pauzez",
-  "video.play": "Pleez",
-  "video.unmute": "Desilencigez sono"
+  "video.play": "Pleez"
 }
diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json
index 7feac4b063..50f31dd623 100644
--- a/app/javascript/mastodon/locales/is.json
+++ b/app/javascript/mastodon/locales/is.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Breyta notandasniði",
   "account.enable_notifications": "Láta mig vita þegar @{name} sendir inn",
   "account.endorse": "Birta á notandasniði",
+  "account.featured": "Með aukið vægi",
+  "account.featured.hashtags": "Myllumerki",
+  "account.featured.posts": "Færslur",
   "account.featured_tags.last_status_at": "Síðasta færsla þann {date}",
   "account.featured_tags.last_status_never": "Engar færslur",
-  "account.featured_tags.title": "Myllumerki hjá {name} með aukið vægi",
   "account.follow": "Fylgjast með",
   "account.follow_back": "Fylgjast með til baka",
   "account.followers": "Fylgjendur",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} færsla} other {{counter} færslur}}",
   "account.unblock": "Aflétta útilokun af @{name}",
   "account.unblock_domain": "Aflétta útilokun lénsins {domain}",
+  "account.unblock_domain_short": "Aflétta útilokun",
   "account.unblock_short": "Hætta að loka á",
   "account.unendorse": "Ekki birta á notandasniði",
   "account.unfollow": "Hætta að fylgja",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Leitarniðurstöður",
   "emoji_button.symbols": "Tákn",
   "emoji_button.travel": "Ferðalög og staðir",
+  "empty_column.account_featured": "Þessi listi er tómur",
   "empty_column.account_hides_collections": "Notandinn hefur valið að gera ekki tiltækar þessar upplýsingar",
   "empty_column.account_suspended": "Notandaaðgangur í frysti",
   "empty_column.account_timeline": "Engar færslur hér!",
@@ -377,6 +381,8 @@
   "generic.saved": "Vistað",
   "getting_started.heading": "Komast í gang",
   "hashtag.admin_moderation": "Opna umsjónarviðmót fyrir #{name}",
+  "hashtag.browse": "Skoða færslur með #{hashtag}",
+  "hashtag.browse_from_account": "Skoða færslur frá @{name} í #{hashtag}",
   "hashtag.column_header.tag_mode.all": "og {additional}",
   "hashtag.column_header.tag_mode.any": "eða {additional}",
   "hashtag.column_header.tag_mode.none": "án {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} færsla} other {{counter} færslur}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} færsla} other {{counter} færslur}} í dag",
   "hashtag.follow": "Fylgjast með myllumerki",
+  "hashtag.mute": "Þagga #{hashtag}",
   "hashtag.unfollow": "Hætta að fylgjast með myllumerki",
   "hashtags.and_other": "…og {count, plural, other {# til viðbótar}}",
   "hints.profiles.followers_may_be_missing": "Fylgjendur frá þessum notanda gæti vantað.",
@@ -598,11 +605,11 @@
   "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Samþykkja beiðni} other {Samþykkja beiðnir}}",
   "notification_requests.confirm_accept_multiple.message": "Þú ert að fara að samþykkja {count, plural, one {eina beiðni um tilkynningar} other {# beiðnir um tilkynningar}}. Ertu viss um að þú viljir halda áfram?",
   "notification_requests.confirm_accept_multiple.title": "Samþykkja beiðnir um tilkynningar?",
-  "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Afgreiða beiðni} other {Afgreiða beiðnir}}",
+  "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Hafna beiðni} other {Hafna beiðnum}}",
   "notification_requests.confirm_dismiss_multiple.message": "Þú ert að fara að hunsa {count, plural, one {eina beiðni um tilkynningar} other {# beiðnir um tilkynningar}}. Þú munt ekki eiga auðvelt með að skoða {count, plural, one {hana} other {þær}} aftur síðar. Ertu viss um að þú viljir halda áfram?",
   "notification_requests.confirm_dismiss_multiple.title": "Hunsa beiðnir um tilkynningar?",
-  "notification_requests.dismiss": "Afgreiða",
-  "notification_requests.dismiss_multiple": "{count, plural, one {Afgreiða # beiðni…} other {Afgreiða # beiðnir…}}",
+  "notification_requests.dismiss": "Hafna",
+  "notification_requests.dismiss_multiple": "{count, plural, one {Hafna # beiðni…} other {Hafna # beiðnum…}}",
   "notification_requests.edit_selection": "Breyta",
   "notification_requests.exit_selection": "Lokið",
   "notification_requests.explainer_for_limited_account": "Tilkynningar frá þessum notanda hafa verið síaðar þar sem aðgangur hans hefur verið takmarkaður af umsjónarmanni.",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Fjarlægja könnun",
   "privacy.change": "Aðlaga gagnaleynd færslu",
   "privacy.direct.long": "Allir sem minnst er á í færslunni",
-  "privacy.direct.short": "Tilteknir aðilar",
+  "privacy.direct.short": "Einkaspjall",
   "privacy.private.long": "Einungis þeir sem fylgjast með þér",
   "privacy.private.short": "Fylgjendur",
   "privacy.public.long": "Hver sem er, á og utan Mastodon",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Breyta tungumálum í áskrift fyrir {target}",
   "tabs_bar.home": "Heim",
   "tabs_bar.notifications": "Tilkynningar",
+  "terms_of_service.effective_as_of": "Gildir frá og með {date}",
   "terms_of_service.title": "Þjónustuskilmálar",
+  "terms_of_service.upcoming_changes_on": "Væntanlegar breytingar þann {date}",
   "time_remaining.days": "{number, plural, one {# dagur} other {# dagar}} eftir",
   "time_remaining.hours": "{number, plural, one {# klukkustund} other {# klukkustundir}} eftir",
   "time_remaining.minutes": "{number, plural, one {# mínúta} other {# mínútur}} eftir",
@@ -903,8 +912,12 @@
   "video.expand": "Stækka myndskeið",
   "video.fullscreen": "Skjáfylli",
   "video.hide": "Fela myndskeið",
-  "video.mute": "Þagga hljóð",
+  "video.mute": "Þagga niður",
   "video.pause": "Gera hlé",
   "video.play": "Spila",
-  "video.unmute": "Kveikja á hljóði"
+  "video.skip_backward": "Stökkva til baka",
+  "video.skip_forward": "Stökkva áfram",
+  "video.unmute": "Hætta að þagga",
+  "video.volume_down": "Lækka hljóðstyrk",
+  "video.volume_up": "Hækka hljóðstyrk"
 }
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index fc52700692..36770c675f 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -27,16 +27,18 @@
   "account.edit_profile": "Modifica profilo",
   "account.enable_notifications": "Avvisami quando @{name} pubblica un post",
   "account.endorse": "In evidenza sul profilo",
+  "account.featured": "In primo piano",
+  "account.featured.hashtags": "Hashtag",
+  "account.featured.posts": "Post",
   "account.featured_tags.last_status_at": "Ultimo post il {date}",
   "account.featured_tags.last_status_never": "Nessun post",
-  "account.featured_tags.title": "Hashtag in evidenza di {name}",
   "account.follow": "Segui",
   "account.follow_back": "Segui a tua volta",
   "account.followers": "Follower",
   "account.followers.empty": "Ancora nessuno segue questo utente.",
   "account.followers_counter": "{count, plural, one {{counter} seguace} other {{counter} seguaci}}",
   "account.following": "Seguiti",
-  "account.following_counter": "{count, plural, one {{counter} segui} other {{counter} segui}}",
+  "account.following_counter": "{count, plural, one {{counter} segui} other {{counter} seguiti}}",
   "account.follows.empty": "Questo utente non segue ancora nessuno.",
   "account.go_to_profile": "Vai al profilo",
   "account.hide_reblogs": "Nascondi condivisioni da @{name}",
@@ -65,12 +67,13 @@
   "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} post}}",
   "account.unblock": "Sblocca @{name}",
   "account.unblock_domain": "Sblocca il dominio {domain}",
+  "account.unblock_domain_short": "Sblocca",
   "account.unblock_short": "Sblocca",
   "account.unendorse": "Non mostrare sul profilo",
   "account.unfollow": "Smetti di seguire",
   "account.unmute": "Riattiva @{name}",
   "account.unmute_notifications_short": "Riattiva notifiche",
-  "account.unmute_short": "Riattiva",
+  "account.unmute_short": "Attiva audio",
   "account_note.placeholder": "Clicca per aggiungere una nota",
   "admin.dashboard.daily_retention": "Tasso di ritenzione dell'utente per giorno, dopo la registrazione",
   "admin.dashboard.monthly_retention": "Tasso di ritenzione dell'utente per mese, dopo la registrazione",
@@ -218,6 +221,10 @@
   "confirmations.logout.confirm": "Disconnettiti",
   "confirmations.logout.message": "Sei sicuro di volerti disconnettere?",
   "confirmations.logout.title": "Uscire?",
+  "confirmations.missing_alt_text.confirm": "Aggiungi testo alternativo",
+  "confirmations.missing_alt_text.message": "Il tuo post contiene media senza testo alternativo. L'aggiunta di descrizioni aiuta a rendere i tuoi contenuti accessibili a più persone.",
+  "confirmations.missing_alt_text.secondary": "Pubblica comunque",
+  "confirmations.missing_alt_text.title": "Aggiungere testo alternativo?",
   "confirmations.mute.confirm": "Silenzia",
   "confirmations.redraft.confirm": "Elimina e riscrivi",
   "confirmations.redraft.message": "Sei sicuro di voler eliminare questo post e riscriverlo? I preferiti e i boost andranno persi e le risposte al post originale non saranno più collegate.",
@@ -289,6 +296,7 @@
   "emoji_button.search_results": "Risultati della ricerca",
   "emoji_button.symbols": "Simboli",
   "emoji_button.travel": "Viaggi & Luoghi",
+  "empty_column.account_featured": "Questa lista è vuota",
   "empty_column.account_hides_collections": "Questo utente ha scelto di non rendere disponibili queste informazioni",
   "empty_column.account_suspended": "Profilo sospeso",
   "empty_column.account_timeline": "Nessun post qui!",
@@ -693,12 +701,12 @@
   "poll_button.remove_poll": "Rimuovi il sondaggio",
   "privacy.change": "Modifica privacy del post",
   "privacy.direct.long": "Tutti quelli menzionati nel post",
-  "privacy.direct.short": "Persone specifiche",
+  "privacy.direct.short": "Menzione privata",
   "privacy.private.long": "Solo i tuoi follower",
   "privacy.private.short": "Follower",
   "privacy.public.long": "Chiunque dentro e fuori Mastodon",
   "privacy.public.short": "Pubblico",
-  "privacy.unlisted.additional": "",
+  "privacy.unlisted.additional": "Si comporta esattamente come pubblico, tranne per il fatto che il post non verrà visualizzato nei feed live o negli hashtag, nell'esplorazione o nella ricerca Mastodon, anche se hai attivato l'attivazione a livello di account.",
   "privacy.unlisted.long": "Meno fanfare algoritmiche",
   "privacy.unlisted.short": "Pubblico silenzioso",
   "privacy_policy.last_updated": "Ultimo aggiornamento {date}",
@@ -868,7 +876,9 @@
   "subscribed_languages.target": "Modifica le lingue in cui sei iscritto per {target}",
   "tabs_bar.home": "Home",
   "tabs_bar.notifications": "Notifiche",
+  "terms_of_service.effective_as_of": "In vigore a partire dal giorno {date}",
   "terms_of_service.title": "Termini di Servizio",
+  "terms_of_service.upcoming_changes_on": "Prossime modifiche nel giorno {date}",
   "time_remaining.days": "{number, plural, one {# giorno} other {# giorni}} left",
   "time_remaining.hours": "{number, plural, one {# ora} other {# ore}} left",
   "time_remaining.minutes": "{number, plural, one {# minuto} other {# minuti}} left",
@@ -899,8 +909,12 @@
   "video.expand": "Espandi il video",
   "video.fullscreen": "Schermo intero",
   "video.hide": "Nascondi il video",
-  "video.mute": "Silenzia suono",
+  "video.mute": "Muta",
   "video.pause": "Pausa",
   "video.play": "Riproduci",
-  "video.unmute": "Riattiva suono"
+  "video.skip_backward": "Vai indietro",
+  "video.skip_forward": "Vai avanti",
+  "video.unmute": "Muta",
+  "video.volume_down": "Abbassa volume",
+  "video.volume_up": "Alza volume"
 }
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 9ac11c145a..a2328401f4 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -40,7 +40,6 @@
   "account.endorse": "プロフィールで紹介する",
   "account.featured_tags.last_status_at": "最終投稿 {date}",
   "account.featured_tags.last_status_never": "投稿がありません",
-  "account.featured_tags.title": "{name}の注目ハッシュタグ",
   "account.follow": "フォロー",
   "account.follow_back": "フォローバック",
   "account.followers": "フォロワー",
@@ -99,8 +98,11 @@
   "alert.unexpected.title": "エラー!",
   "alt_text_badge.title": "代替テキスト",
   "alt_text_modal.add_alt_text": "代替テキストを追加",
+  "alt_text_modal.add_text_from_image": "画像からテキストを追加",
   "alt_text_modal.cancel": "キャンセル",
   "alt_text_modal.change_thumbnail": "サムネイルを変更",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "耳の不自由な方のために説明してください…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "目が不自由な方のために説明してください…",
   "alt_text_modal.done": "完了",
   "announcement.announcement": "お知らせ",
   "annual_report.summary.archetype.booster": "トレンドハンター",
@@ -334,6 +336,10 @@
   "confirmations.logout.confirm": "ログアウト",
   "confirmations.logout.message": "本当にログアウトしますか?",
   "confirmations.logout.title": "ログアウトしようとしています",
+  "confirmations.missing_alt_text.confirm": "代替テキストを追加",
+  "confirmations.missing_alt_text.message": "あなたの投稿には代替テキストのないメディアが含まれています。説明文を追加することで、より多くの人がコンテンツにアクセスできるようになります。",
+  "confirmations.missing_alt_text.secondary": "そのまま投稿する",
+  "confirmations.missing_alt_text.title": "代替テキストを追加しますか?",
   "confirmations.mute.confirm": "ミュート",
   "confirmations.redraft.confirm": "削除して下書きに戻す",
   "confirmations.redraft.message": "投稿を削除して下書きに戻します。この投稿へのお気に入り登録やブーストは失われ、返信は孤立することになります。よろしいですか?",
@@ -590,6 +596,7 @@
   "keyboard_shortcuts.toggle_hidden": "CWで隠れた文を見る/隠す",
   "keyboard_shortcuts.toggle_sensitivity": "非表示のメディアを見る/隠す",
   "keyboard_shortcuts.toot": "新規投稿",
+  "keyboard_shortcuts.translate": "投稿を翻訳する",
   "keyboard_shortcuts.unfocus": "投稿の入力欄・検索欄から離れる",
   "keyboard_shortcuts.up": "カラム内一つ上に移動",
   "lightbox.close": "閉じる",
@@ -840,7 +847,7 @@
   "privacy.circle.long": "サークルメンバーのみ閲覧可",
   "privacy.circle.short": "サークル (投稿時点)",
   "privacy.direct.long": "本文で指定した相手のみ",
-  "privacy.direct.short": "特定の人",
+  "privacy.direct.short": "非公開の返信",
   "privacy.limited.short": "限定投稿",
   "privacy.login.long": "ログインユーザーのみ閲覧可、公開",
   "privacy.login.short": "ログインユーザーのみ",
@@ -1092,5 +1099,9 @@
   "video.mute": "ミュート",
   "video.pause": "一時停止",
   "video.play": "再生",
-  "video.unmute": "ミュートを解除する"
+  "video.skip_backward": "後方にスキップ",
+  "video.skip_forward": "前方にスキップ",
+  "video.unmute": "ミュート解除",
+  "video.volume_down": "音量を下げる",
+  "video.volume_up": "音量を上げる"
 }
diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json
index d1587e1c2c..d078ef5cab 100644
--- a/app/javascript/mastodon/locales/ka.json
+++ b/app/javascript/mastodon/locales/ka.json
@@ -6,20 +6,20 @@
   "account.badges.group": "ჯგუფი",
   "account.block": "დაბლოკე @{name}",
   "account.block_domain": "დაიმალოს ყველაფერი დომენიდან {domain}",
-  "account.blocked": "დაიბლოკა",
+  "account.blocked": "დაბლოკილია",
   "account.cancel_follow_request": "Withdraw follow request",
   "account.domain_blocked": "დომენი დამალულია",
   "account.edit_profile": "პროფილის ცვლილება",
   "account.endorse": "გამორჩევა პროფილზე",
-  "account.featured_tags.last_status_never": "პოსტები არ არის",
+  "account.featured_tags.last_status_never": "პოსტების გარეშე",
   "account.follow": "გაყოლა",
   "account.followers": "მიმდევრები",
   "account.hide_reblogs": "დაიმალოს ბუსტები @{name}-სგან",
   "account.media": "მედია",
   "account.mention": "ასახელეთ @{name}",
   "account.mute": "გააჩუმე @{name}",
-  "account.muted": "გაჩუმებული",
-  "account.posts": "ტუტები",
+  "account.muted": "დადუმებულია",
+  "account.posts": "პოსტები",
   "account.posts_with_replies": "ტუტები და პასუხები",
   "account.report": "დაარეპორტე @{name}",
   "account.requested": "დამტკიცების მოლოდინში. დააწკაპუნეთ რომ უარყოთ დადევნების მოთხონვა",
@@ -42,7 +42,7 @@
   "column.community": "ლოკალური თაიმლაინი",
   "column.domain_blocks": "დამალული დომენები",
   "column.follow_requests": "დადევნების მოთხოვნები",
-  "column.home": "სახლი",
+  "column.home": "საწყისი",
   "column.lists": "სიები",
   "column.mutes": "გაჩუმებული მომხმარებლები",
   "column.notifications": "შეტყობინებები",
@@ -52,9 +52,9 @@
   "column_header.hide_settings": "პარამეტრების დამალვა",
   "column_header.moveLeft_settings": "სვეტის მარცხნივ გადატანა",
   "column_header.moveRight_settings": "სვეტის მარჯვნივ გადატანა",
-  "column_header.pin": "აპინვა",
+  "column_header.pin": "მიმაგრება",
   "column_header.show_settings": "პარამეტრების ჩვენება",
-  "column_header.unpin": "პინის მოხსნა",
+  "column_header.unpin": "მოხსნა",
   "column_subheading.settings": "პარამეტრები",
   "community.column_settings.media_only": "მხოლოდ მედია",
   "compose_form.direct_message_warning_learn_more": "გაიგე მეტი",
@@ -66,21 +66,21 @@
   "compose_form.publish_form": "Publish",
   "compose_form.spoiler.marked": "გაფრთხილების უკან ტექსტი დამალულია",
   "compose_form.spoiler.unmarked": "ტექსტი არაა დამალული",
-  "confirmation_modal.cancel": "უარყოფა",
+  "confirmation_modal.cancel": "გაუქმება",
   "confirmations.block.confirm": "ბლოკი",
-  "confirmations.delete.confirm": "გაუქმება",
+  "confirmations.delete.confirm": "წაშლა",
   "confirmations.delete.message": "დარწმუნებული ხართ, გსურთ გააუქმოთ ეს სტატუსი?",
-  "confirmations.delete_list.confirm": "გაუქმება",
+  "confirmations.delete_list.confirm": "წაშლა",
   "confirmations.delete_list.message": "დარწმუნებული ხართ, გსურთ სამუდამოდ გააუქმოთ ეს სია?",
-  "confirmations.mute.confirm": "გაჩუმება",
+  "confirmations.mute.confirm": "დადუმება",
   "confirmations.redraft.confirm": "გაუქმება და გადანაწილება",
   "confirmations.unfollow.confirm": "ნუღარ მიჰყვები",
   "confirmations.unfollow.message": "დარწმუნებული ხართ, აღარ გსურთ მიჰყვებოდეთ {name}-ს?",
   "embed.instructions": "ეს სტატუსი ჩასვით თქვენს ვებ-საიტზე შემდეგი კოდის კოპირებით.",
   "embed.preview": "ესაა თუ როგორც გამოჩნდება:",
   "emoji_button.activity": "აქტივობა",
-  "emoji_button.custom": "პერსონალიზირებული",
-  "emoji_button.flags": "დროშები",
+  "emoji_button.custom": "მომხმარებლის",
+  "emoji_button.flags": "ალმები",
   "emoji_button.food": "საჭმელი და სასლმელი",
   "emoji_button.label": "ემოჯის ჩასმა",
   "emoji_button.nature": "ბუმება",
@@ -119,7 +119,7 @@
   "keyboard_shortcuts.federated": "to open federated timeline",
   "keyboard_shortcuts.heading": "კლავიატურის სწრაფი ბმულები",
   "keyboard_shortcuts.home": "to open home timeline",
-  "keyboard_shortcuts.hotkey": "ცხელი კლავიში",
+  "keyboard_shortcuts.hotkey": "მალსახმობი ღილაკი",
   "keyboard_shortcuts.legend": "ამ ლეგენდის გამოსაჩენად",
   "keyboard_shortcuts.local": "to open local timeline",
   "keyboard_shortcuts.mention": "ავტორის დასახელებლად",
@@ -180,20 +180,20 @@
   "relative_time.just_now": "ახლა",
   "relative_time.minutes": "{number}წთ",
   "relative_time.seconds": "{number}წმ",
-  "reply_indicator.cancel": "უარყოფა",
+  "reply_indicator.cancel": "გაუქმება",
   "report.forward": "ფორვარდი {target}-ს",
   "report.forward_hint": "ანგარიში სხვა სერვერიდანაა. გავაგზავნოთ რეპორტის ანონიმური ასლიც?",
   "report.placeholder": "დამატებითი კომენტარები",
-  "report.submit": "დასრულება",
+  "report.submit": "გადაცემა",
   "report.target": "არეპორტებთ {target}",
   "report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached",
   "search.placeholder": "ძებნა",
   "search_results.hashtags": "ჰეშტეგები",
-  "search_results.statuses": "ტუტები",
-  "sign_in_banner.sign_in": "Sign in",
+  "search_results.statuses": "პოსტები",
+  "sign_in_banner.sign_in": "შესვლა",
   "status.admin_status": "Open this status in the moderation interface",
   "status.block": "დაბლოკე @{name}",
-  "status.cancel_reblog_private": "ბუსტის მოშორება",
+  "status.cancel_reblog_private": "ბუსტის მოხსნა",
   "status.cannot_reblog": "ეს პოსტი ვერ დაიბუსტება",
   "status.copy": "Copy link to status",
   "status.delete": "წაშლა",
@@ -222,7 +222,7 @@
   "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
   "status.unmute_conversation": "საუბარზე გაჩუმების მოშორება",
   "status.unpin": "პროფილიდან პინის მოშორება",
-  "tabs_bar.home": "სახლი",
+  "tabs_bar.home": "საწყისი",
   "tabs_bar.notifications": "შეტყობინებები",
   "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}",
   "ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.",
@@ -234,8 +234,6 @@
   "video.expand": "ვიდეოს გაფართოება",
   "video.fullscreen": "ჩვენება სრულ ეკრანზე",
   "video.hide": "ვიდეოს დამალვა",
-  "video.mute": "ხმის გაჩუმება",
   "video.pause": "პაუზა",
-  "video.play": "დაკვრა",
-  "video.unmute": "ხმის გაჩუმების მოშორება"
+  "video.play": "დაკვრა"
 }
diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json
index 8cdfe6df19..bb97a18bd2 100644
--- a/app/javascript/mastodon/locales/kab.json
+++ b/app/javascript/mastodon/locales/kab.json
@@ -5,7 +5,7 @@
   "about.domain_blocks.no_reason_available": "Ulac taɣẓint",
   "about.domain_blocks.preamble": "Maṣṭudun s umata yeḍmen-ak ad teẓreḍ agbur, ad tesdemreḍ akked yimseqdacen-nniḍen seg yal aqeddac deg fedivers. Ha-tent-an ɣur-k tsuraf i yellan deg uqeddac-agi.",
   "about.domain_blocks.silenced.title": "Ɣur-s talast",
-  "about.domain_blocks.suspended.title": "Yeḥbes",
+  "about.domain_blocks.suspended.title": "Yettwaḥbes",
   "about.not_available": "Talɣut-a ur tettwabder ara deg uqeddac-a.",
   "about.powered_by": "Azeṭṭa inmetti yettwasɣelsen sɣur {mastodon}",
   "about.rules": "Ilugan n uqeddac",
@@ -20,10 +20,10 @@
   "account.cancel_follow_request": "Sefsex taḍfart",
   "account.copy": "Nɣel assaɣ ɣer umaɣnu",
   "account.direct": "Bder-d @{name} weḥd-s",
-  "account.disable_notifications": "Ḥbes ur iyi-d-ttazen ara alɣuten mi ara d-isuffeɣ @{name}",
+  "account.disable_notifications": "Ḥbes ur iyi-d-ttazen ara ilɣa mi ara d-isuffeɣ @{name}",
   "account.domain_blocked": "Taɣult yeffren",
   "account.edit_profile": "Ẓreg amaɣnu",
-  "account.enable_notifications": "Azen-iyi-d alɣuten mi ara d-isuffeɣ @{name}",
+  "account.enable_notifications": "Azen-iyi-d ilɣa mi ara d-isuffeɣ @{name}",
   "account.endorse": "Welleh fell-as deg umaɣnu-inek",
   "account.featured_tags.last_status_at": "Tasuffeɣt taneggarut ass n {date}",
   "account.featured_tags.last_status_never": "Ulac tisuffaɣ",
@@ -45,7 +45,7 @@
   "account.mention": "Bder-d @{name}",
   "account.moved_to": "{name} yenna-d dakken amiḍan-is amaynut yuɣal :",
   "account.mute": "Sgugem @{name}",
-  "account.mute_notifications_short": "Susem alɣuten",
+  "account.mute_notifications_short": "Susem ilɣa",
   "account.mute_short": "Sgugem",
   "account.muted": "Yettwasgugem",
   "account.mutual": "Temṭafarem",
@@ -61,6 +61,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} n tsuffeɣt} other {{counter} n tsuffaɣ}}",
   "account.unblock": "Serreḥ i @{name}",
   "account.unblock_domain": "Ssken-d {domain}",
+  "account.unblock_domain_short": "Serreḥ",
   "account.unblock_short": "Serreḥ",
   "account.unendorse": "Ur ttwellih ara fell-as deg umaɣnu-inek",
   "account.unfollow": "Ur ṭṭafaṛ ara",
@@ -73,11 +74,15 @@
   "alert.rate_limited.title": "Aktum s talast",
   "alert.unexpected.message": "Yeḍra-d unezri ur netturaǧu ara.",
   "alert.unexpected.title": "Ayhuh!",
-  "alt_text_badge.title": "Aḍris asegzan",
+  "alt_text_badge.title": "Aḍris amlellay",
+  "alt_text_modal.add_alt_text": "Rnu aḍris amlellay",
+  "alt_text_modal.add_text_from_image": "Rnu aḍris amlellay seg tugna",
   "alt_text_modal.cancel": "Semmet",
   "alt_text_modal.done": "Immed",
   "announcement.announcement": "Ulɣu",
+  "annual_report.summary.most_used_app.most_used_app": "asnas yettwasqedcen s waṭas",
   "annual_report.summary.most_used_hashtag.none": "Ula yiwen",
+  "annual_report.summary.new_posts.new_posts": "tisuffaɣ timaynutin",
   "audio.hide": "Ffer amesli",
   "block_modal.show_less": "Ssken-d drus",
   "block_modal.show_more": "Ssken-d ugar",
@@ -113,7 +118,7 @@
   "column.home": "Agejdan",
   "column.lists": "Tibdarin",
   "column.mutes": "Imiḍanen yettwasgugmen",
-  "column.notifications": "Alɣuten",
+  "column.notifications": "Ilɣa",
   "column.pins": "Tisuffaɣ yettwasenṭḍen",
   "column.public": "Tasuddemt tamatut",
   "column_back_button.label": "Tuɣalin",
@@ -123,6 +128,7 @@
   "column_header.pin": "Senteḍ",
   "column_header.show_settings": "Ssken iɣewwaṛen",
   "column_header.unpin": "Kkes asenteḍ",
+  "column_search.cancel": "Semmet",
   "column_subheading.settings": "Iɣewwaṛen",
   "community.column_settings.local_only": "Adigan kan",
   "community.column_settings.media_only": "Imidyaten kan",
@@ -141,6 +147,7 @@
   "compose_form.poll.duration": "Tanzagt n tefrant",
   "compose_form.poll.multiple": "Aṭas n ufran",
   "compose_form.poll.option_placeholder": "Taxtiṛt {number}",
+  "compose_form.poll.single": "Yiwen wefran",
   "compose_form.poll.type": "Aɣanib",
   "compose_form.publish": "Suffeɣ",
   "compose_form.publish_form": "Tasuffeɣt tamaynut",
@@ -159,9 +166,13 @@
   "confirmations.discard_edit_media.confirm": "Sefsex",
   "confirmations.edit.confirm": "Ẓreg",
   "confirmations.edit.message": "Abeddel tura ad d-yaru izen-nni i d-tegreḍ akka tura. Tetḥeqqeḍ tebɣiḍ ad tkemmleḍ?",
+  "confirmations.follow_to_list.confirm": "Ḍfeṛ-it sakin rnu-t ɣer tebdart",
   "confirmations.logout.confirm": "Ffeɣ",
   "confirmations.logout.message": "D tidet tebɣiḍ ad teffɣeḍ?",
   "confirmations.logout.title": "Tebɣiḍ ad teffɣeḍ ssya?",
+  "confirmations.missing_alt_text.confirm": "Rnu aḍris amlellay",
+  "confirmations.missing_alt_text.secondary": "Suffeɣ akken yebɣu yili",
+  "confirmations.missing_alt_text.title": "Rnu aḍris amlellay?",
   "confirmations.mute.confirm": "Sgugem",
   "confirmations.redraft.confirm": "Kkes sakin ɛiwed tira",
   "confirmations.reply.confirm": "Err",
@@ -222,7 +233,7 @@
   "empty_column.home": "Tasuddemt tagejdant n yisallen d tilemt! Ẓer {public} neɣ nadi ad tafeḍ imseqdacen-nniḍen ad ten-ḍefṛeḍ.",
   "empty_column.list": "Ar tura ur yelli kra deg umuɣ-a. Ad d-yettwasken da ticki iɛeggalen n wumuɣ-a suffɣen-d kra.",
   "empty_column.mutes": "Ulac ɣur-k·m imseqdacen i yettwasgugmen.",
-  "empty_column.notifications": "Ulac ɣur-k·m alɣuten. Sedmer akked yemdanen-nniḍen akken ad tebduḍ adiwenni.",
+  "empty_column.notifications": "Ulac ɣur-k·m ilɣa. Sedmer akked yemdanen-nniḍen akken ad tebduḍ adiwenni.",
   "empty_column.public": "Ulac kra da! Aru kra, neɣ ḍfeṛ imdanen i yellan deg yiqeddacen-nniḍen akken ad d-teččar tsuddemt tazayezt",
   "error.unexpected_crash.next_steps": "Smiren asebter-a, ma ur yekkis ara wugur, ẓer d akken tzemreḍ ad tesqedceḍ Maṣṭudun deg yiminig-nniḍen neɣ deg usnas anaṣli.",
   "errors.unexpected_crash.copy_stacktrace": "Nɣel stacktrace ɣef wafus",
@@ -241,6 +252,7 @@
   "filter_modal.select_filter.search": "Nadi neɣ snulfu-d",
   "filter_modal.select_filter.title": "Sizdeg tassufeɣt-a",
   "filter_modal.title.status": "Sizdeg tassufeɣt",
+  "filtered_notifications_banner.title": "Ilɣa yettwasizdgen",
   "firehose.all": "Akk",
   "firehose.local": "Deg uqeddac-ayi",
   "firehose.remote": "Iqeddacen nniḍen",
@@ -248,8 +260,11 @@
   "follow_request.reject": "Agi",
   "follow_suggestions.dismiss": "Dayen ur t-id-skan ara",
   "follow_suggestions.featured_longer": "Yettwafraned s ufus sɣur agraw n {domain}",
+  "follow_suggestions.friends_of_friends_longer": "D aɣeṛfan ar wid i teṭṭafareḍ",
   "follow_suggestions.hints.featured": "Amaɣnu-a ifren-it-id wegraw n {domain} s ufus.",
-  "follow_suggestions.popular_suggestion_longer": "Yettwassen deg {domain}",
+  "follow_suggestions.hints.friends_of_friends": "Amaɣnu-a d aɣeṛfan ɣer wid i teṭṭafaṛeḍ.",
+  "follow_suggestions.popular_suggestion": "Asumer aɣeṛfan",
+  "follow_suggestions.popular_suggestion_longer": "D aɣeṛfan deg {domain}",
   "follow_suggestions.view_all": "Wali-ten akk",
   "follow_suggestions.who_to_follow": "Ad tḍefreḍ?",
   "followed_tags": "Ihacṭagen yettwaḍfaren",
@@ -260,6 +275,7 @@
   "footer.privacy_policy": "Tasertit tabaḍnit",
   "footer.source_code": "Wali tangalt taɣbalut",
   "footer.status": "Addad",
+  "footer.terms_of_service": "Tiwtilin n useqdec",
   "generic.saved": "Yettwasekles",
   "getting_started.heading": "Bdu",
   "hashtag.column_header.tag_mode.all": "d {additional}",
@@ -273,7 +289,7 @@
   "hashtag.column_settings.tag_toggle": "Glu-d s yihacṭagen imerna i ujgu-agi",
   "hashtag.counter_by_accounts": "{count, plural, one {{counter} imtekki} other {{counter} n imtekkiyen}}",
   "hashtag.counter_by_uses": "{count, plural, one {{counter} n tsuffeɣt} other {{counter} n tsuffaɣ}}",
-  "hashtag.counter_by_uses_today": "{count, plural, one {{counter} n tsuffeɣt} other {{counter} n tsuffaɣ}} assa",
+  "hashtag.counter_by_uses_today": "{count, plural, one {{counter} n tsuffeɣt} other {{counter} n tsuffaɣ}} ass-a",
   "hashtag.follow": "Ḍfeṛ ahacṭag",
   "hashtags.and_other": "…d {count, plural, one {}other {# nniḍen}}",
   "hints.threads.replies_may_be_missing": "Tiririyin d-yusan deg iqeddacen nniḍen, yezmer ur d-ddant ara.",
@@ -284,10 +300,19 @@
   "home.pending_critical_update.body": "Ma ulac aɣilif, leqqem aqeddac-ik Mastodon akken kan tzemreḍ !",
   "home.pending_critical_update.link": "Wali ileqman",
   "home.show_announcements": "Ssken-d ulɣuyen",
+  "info_button.label": "Tallelt",
+  "interaction_modal.action.favourite": "I wakken ad tkemmleḍ, yessefk ad d-tḥemmleḍ seg umiḍan-ik·im.",
+  "interaction_modal.action.follow": "I wakken ad tkemmleḍ, yessefk ad d-tḍefreḍ seg umiḍan-ik·im.",
+  "interaction_modal.action.reblog": "I wakken ad tkemmleḍ, yessefk ad d-snernuḍ seg umiḍan-ik·im.",
+  "interaction_modal.action.reply": "I wakken ad tkemmleḍ, yessefk ad d-terreḍ seg umiḍan-ik·im.",
+  "interaction_modal.action.vote": "I wakken ad tkemmleḍ, yessefk ad d-tferneḍ seg umiḍan-ik·im.",
+  "interaction_modal.go": "Ddu",
+  "interaction_modal.no_account_yet": "Werɛad ur tesɛid amiḍan?",
   "interaction_modal.on_another_server": "Deg uqeddac nniḍen",
   "interaction_modal.on_this_server": "Deg uqeddac-ayi",
   "interaction_modal.title.follow": "Ḍfer {name}",
   "interaction_modal.title.reply": "Tiririt i tsuffeɣt n {name}",
+  "interaction_modal.username_prompt": "Amedya: {example}",
   "intervals.full.days": "{number, plural, one {# wass} other {# wussan}}",
   "intervals.full.hours": "{number, plural, one {# usarag} other {# yisragen}}",
   "intervals.full.minutes": "{number, plural, one {# n tesdat} other {# n tesdatin}}",
@@ -310,7 +335,7 @@
   "keyboard_shortcuts.mention": "akken ad d-bedreḍ ameskar",
   "keyboard_shortcuts.muted": "akken ad teldiḍ tabdart n yimseqdacen yettwasgugmen",
   "keyboard_shortcuts.my_profile": "akken ad d-teldiḍ amaɣnu-ik",
-  "keyboard_shortcuts.notifications": "akken ad d-teldiḍ ajgu n walɣuten",
+  "keyboard_shortcuts.notifications": "Ad d-yeldi ajgu n yilɣa",
   "keyboard_shortcuts.open_media": "i tiɣwalin yeldin",
   "keyboard_shortcuts.pinned": "akken ad teldiḍ tabdart n tjewwiqin yettwasentḍen",
   "keyboard_shortcuts.profile": "akken ad d-teldiḍ amaɣnu n umeskar",
@@ -336,9 +361,12 @@
   "lists.add_to_lists": "Rnu {name} ɣer tebdarin",
   "lists.create": "Snulfu-d",
   "lists.delete": "Kkes tabdart",
+  "lists.done": "Immed",
   "lists.edit": "Ẓreg tabdart",
   "lists.list_name": "Isem n tebdart",
   "lists.new_list_name": "Isem n tebdart tamaynut",
+  "lists.no_lists_yet": "Ulac tibdarin akka tura.",
+  "lists.no_results_found": "Ulac igemmad.",
   "lists.remove_member": "Kkes",
   "lists.replies_policy.followed": "Kra n useqdac i yettwaḍefren",
   "lists.replies_policy.list": "Iɛeggalen n tebdart",
@@ -375,6 +403,7 @@
   "navigation_bar.follows_and_followers": "Imeḍfaṛen akked wid i teṭṭafaṛeḍ",
   "navigation_bar.lists": "Tibdarin",
   "navigation_bar.logout": "Ffeɣ",
+  "navigation_bar.moderation": "Aseɣyed",
   "navigation_bar.mutes": "Iseqdacen yettwasusmen",
   "navigation_bar.opened_in_classic_interface": "Tisuffaɣ, imiḍanen akked isebtar-nniḍen igejdanen ldin-d s wudem amezwer deg ugrudem web aklasiki.",
   "navigation_bar.personal": "Udmawan",
@@ -388,6 +417,7 @@
   "notification.admin.sign_up": "Ijerred {name}",
   "notification.favourite": "{name} yesmenyaf addad-ik·im",
   "notification.follow": "iṭṭafar-ik·em-id {name}",
+  "notification.follow.name_and_others": "{name} akked <a>{count, plural, one {# nniḍen} other {# nniḍen}}</a> iḍfeṛ-k·m-id",
   "notification.follow_request": "{name} yessuter-d ad k·m-yeḍfeṛ",
   "notification.label.mention": "Abdar",
   "notification.label.private_mention": "Abdar uslig",
@@ -404,11 +434,12 @@
   "notification_requests.dismiss": "Agi",
   "notification_requests.edit_selection": "Ẓreg",
   "notification_requests.exit_selection": "Immed",
-  "notification_requests.notifications_from": "Alɣuten sɣur {name}",
-  "notifications.clear": "Sfeḍ alɣuten",
-  "notifications.clear_confirmation": "Tebɣiḍ s tidet ad tekkseḍ akk alɣuten-inek·em i lebda?",
+  "notification_requests.notifications_from": "Ilɣa sɣur {name}",
+  "notification_requests.title": "Ilɣa yettwasizdgen",
+  "notifications.clear": "Sfeḍ ilɣa",
+  "notifications.clear_confirmation": "Tebɣiḍ s tidet ad tekkseḍ akk ilɣa-inek·em i lebda?",
   "notifications.column_settings.admin.report": "Ineqqisen imaynuten:",
-  "notifications.column_settings.alert": "Alɣuten n tnarit",
+  "notifications.column_settings.alert": "Ilɣa n tnarit",
   "notifications.column_settings.favourite": "Imenyafen:",
   "notifications.column_settings.filter_bar.advanced": "Sken-d akk taggayin",
   "notifications.column_settings.filter_bar.category": "Iri n usizdeg uzrib",
@@ -417,12 +448,12 @@
   "notifications.column_settings.group": "Agraw",
   "notifications.column_settings.mention": "Abdar:",
   "notifications.column_settings.poll": "Igemmaḍ n usenqed:",
-  "notifications.column_settings.push": "Alɣuten yettudemmren",
+  "notifications.column_settings.push": "Ilɣa yettudemmren",
   "notifications.column_settings.reblog": "Seǧhed:",
   "notifications.column_settings.show": "Ssken-d tilɣa deg ujgu",
   "notifications.column_settings.sound": "Rmed imesli",
   "notifications.column_settings.status": "Tisuffaɣ timaynutin :",
-  "notifications.column_settings.unread_notifications.category": "Alɣuten ur nettwaɣra",
+  "notifications.column_settings.unread_notifications.category": "Ilɣa ur nettwaɣra",
   "notifications.column_settings.update": "Iẓreg:",
   "notifications.filter.all": "Akk",
   "notifications.filter.boosts": "Seǧhed",
@@ -432,10 +463,11 @@
   "notifications.filter.polls": "Igemmaḍ n usenqed",
   "notifications.filter.statuses": "Ileqman n yimdanen i teṭṭafareḍ",
   "notifications.grant_permission": "Mudd tasiregt.",
-  "notifications.group": "{count} n walɣuten",
-  "notifications.mark_as_read": "Creḍ meṛṛa alɣuten am wakken ttwaɣran",
-  "notifications.permission_denied": "D awezɣi ad yili wermad n walɣuten n tnarit axateṛ turagt tettwagdel",
+  "notifications.group": "{count} n yilɣa",
+  "notifications.mark_as_read": "Creḍ akk ilɣa am wakken ttwaɣran",
+  "notifications.permission_denied": "D awezɣi ad yili wermad n yilɣa n tnarit axateṛ turagt tettwagdel",
   "notifications.policy.drop": "Anef-as",
+  "notifications.policy.filter": "Sizdeg",
   "notifications.policy.filter_new_accounts.hint": "Imiḍanen imaynuten i d-yennulfan deg {days, plural, one {yiwen n wass} other {# n wussan}} yezrin",
   "notifications.policy.filter_new_accounts_title": "Imiḍan imaynuten",
   "notifications.policy.filter_not_followers_hint": "Ula d wid akked tid i k·m-id-iḍefren, ur wwiḍen ara {days, plural, one {yiwen wass} other {# wussan}}",
@@ -443,11 +475,12 @@
   "notifications.policy.filter_not_following_hint": "Alamma tqebleḍ-ten s ufus",
   "notifications.policy.filter_not_following_title": "Wid akked tid ur tettḍafareḍ ara",
   "notifications.policy.filter_private_mentions_title": "Abdar uslig ur yettwasferken ara",
-  "notifications_permission_banner.enable": "Rmed alɣuten n tnarit",
+  "notifications_permission_banner.enable": "Rmed ilɣa n tnarit",
   "notifications_permission_banner.title": "Ur zeggel acemma",
   "onboarding.follows.back": "Uɣal",
   "onboarding.follows.done": "Immed",
   "onboarding.follows.search": "Nadi",
+  "onboarding.follows.title": "Ḍfeṛ walbɛaḍ i wakken ad ttebdud",
   "onboarding.profile.display_name": "Isem ara d-yettwaskanen",
   "onboarding.profile.display_name_hint": "Isem-ik·im ummid neɣ isem-ik·im n uqeṣṣer…",
   "onboarding.profile.note": "Tameddurt",
@@ -469,23 +502,25 @@
   "poll_button.remove_poll": "Kkes asenqed",
   "privacy.change": "Seggem tabaḍnit n yizen",
   "privacy.direct.long": "Wid akk i d-yettwabdaren deg tsuffeɣt",
-  "privacy.direct.short": "Imdanen yettwafernen",
+  "privacy.direct.short": "Abdar uslig",
   "privacy.private.long": "Ala wid i k·m-yeṭṭafaṛen",
   "privacy.private.short": "Imeḍfaren",
   "privacy.public.long": "Kra n win yellan deg Masṭudun neɣ berra-s",
   "privacy.public.short": "Azayez",
   "privacy.unlisted.long": "Kra kan yiwarzimen",
+  "privacy.unlisted.short": "Azayez asusam",
   "privacy_policy.last_updated": "Aleqqem aneggaru {date}",
   "privacy_policy.title": "Tasertit tabaḍnit",
   "recommended": "Yettuwelleh",
   "refresh": "Smiren",
+  "regeneration_indicator.please_stand_by": "Ttxil rǧu.",
   "relative_time.days": "{number}u",
   "relative_time.full.just_now": "tura kan",
   "relative_time.hours": "{number}isr",
   "relative_time.just_now": "tura",
   "relative_time.minutes": "{number}tis",
   "relative_time.seconds": "{number}tas",
-  "relative_time.today": "assa",
+  "relative_time.today": "ass-a",
   "reply_indicator.cancel": "Sefsex",
   "reply_indicator.poll": "Afmiḍi",
   "report.block": "Sewḥel",
@@ -535,6 +570,7 @@
   "search.quick_action.status_search": "Tisuffaɣ mṣadan d {x}",
   "search.search_or_paste": "Nadi neɣ senṭeḍ URL",
   "search_popout.full_text_search_disabled_message": "Ur yelli ara deg {domain}.",
+  "search_popout.full_text_search_logged_out_message": "Yella kan mi ara tiliḍ d uqqin.",
   "search_popout.language_code": "Tangalt ISO n tutlayt",
   "search_popout.options": "Iwellihen n unadi",
   "search_popout.quick_actions": "Tigawin tiruradin",
@@ -546,6 +582,7 @@
   "search_results.no_results": "Ulac igemmaḍ.",
   "search_results.see_all": "Wali-ten akk",
   "search_results.statuses": "Tisuffaɣ",
+  "search_results.title": "Igemmaḍ n unadi ɣef \"{q}\"",
   "server_banner.active_users": "iseqdacen urmiden",
   "server_banner.administered_by": "Yettwadbel sɣur :",
   "server_banner.server_stats": "Tidaddanin n uqeddac:",
@@ -606,7 +643,8 @@
   "status.unpin": "Kkes asenteḍ seg umaɣnu",
   "subscribed_languages.save": "Sekles ibeddilen",
   "tabs_bar.home": "Agejdan",
-  "tabs_bar.notifications": "Alɣuten",
+  "tabs_bar.notifications": "Ilɣa",
+  "terms_of_service.title": "Tiwtilin n useqdec",
   "time_remaining.days": "Mazal {number, plural, one {# wass} other {# wussan}}",
   "time_remaining.hours": "Mazal {number, plural, one {# usarag} other {# yisragen}}",
   "time_remaining.minutes": "Mazal {number, plural, one {# n tesdat} other {# n tesdatin}}",
@@ -632,8 +670,6 @@
   "video.expand": "Semɣeṛ tavidyut",
   "video.fullscreen": "Agdil aččuran",
   "video.hide": "Ffer tabidyutt",
-  "video.mute": "Gzem imesli",
   "video.pause": "Sgunfu",
-  "video.play": "Seddu",
-  "video.unmute": "Rmed imesli"
+  "video.play": "Seddu"
 }
diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json
index b7552a2d3e..adc3cdc230 100644
--- a/app/javascript/mastodon/locales/kk.json
+++ b/app/javascript/mastodon/locales/kk.json
@@ -25,7 +25,6 @@
   "account.enable_notifications": "@{name} постары туралы ескерту",
   "account.endorse": "Профильде ұсыну",
   "account.featured_tags.last_status_never": "Пост жоқ",
-  "account.featured_tags.title": "{name} таңдаулы хэштегтері",
   "account.follow": "Жазылу",
   "account.followers": "Жазылушы",
   "account.followers.empty": "Бұл қолданушыға әлі ешкім жазылмаған.",
@@ -337,8 +336,6 @@
   "video.expand": "Видеоны аш",
   "video.fullscreen": "Толық экран",
   "video.hide": "Видеоны жасыр",
-  "video.mute": "Дыбысын бас",
   "video.pause": "Пауза",
-  "video.play": "Қосу",
-  "video.unmute": "Дауысын аш"
+  "video.play": "Қосу"
 }
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 88f35cfc30..544278e519 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "프로필 편집",
   "account.enable_notifications": "@{name} 의 게시물 알림 켜기",
   "account.endorse": "프로필에 추천하기",
+  "account.featured": "추천",
+  "account.featured.hashtags": "해시태그",
+  "account.featured.posts": "게시물",
   "account.featured_tags.last_status_at": "{date}에 마지막으로 게시",
   "account.featured_tags.last_status_never": "게시물 없음",
-  "account.featured_tags.title": "{name} 님의 추천 해시태그",
   "account.follow": "팔로우",
   "account.follow_back": "맞팔로우",
   "account.followers": "팔로워",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, other {게시물 {counter}개}}",
   "account.unblock": "차단 해제",
   "account.unblock_domain": "도메인 {domain} 차단 해제",
+  "account.unblock_domain_short": "차단 해제",
   "account.unblock_short": "차단 해제",
   "account.unendorse": "프로필에 추천하지 않기",
   "account.unfollow": "언팔로우",
@@ -218,6 +221,10 @@
   "confirmations.logout.confirm": "로그아웃",
   "confirmations.logout.message": "정말로 로그아웃 하시겠습니까?",
   "confirmations.logout.title": "로그아웃 할까요?",
+  "confirmations.missing_alt_text.confirm": "대체 텍스트 추가",
+  "confirmations.missing_alt_text.message": "대체 텍스트가 없는 미디어를 포함하고 있습니다. 설명을 추가하면 더 많은 사람들이 내 콘텐츠에 접근할 수 있습니다.",
+  "confirmations.missing_alt_text.secondary": "그냥 게시하기",
+  "confirmations.missing_alt_text.title": "대체 텍스트를 추가할까요?",
   "confirmations.mute.confirm": "뮤트",
   "confirmations.redraft.confirm": "삭제하고 다시 쓰기",
   "confirmations.redraft.message": "정말로 이 게시물을 삭제하고 다시 쓰시겠습니까? 해당 게시물에 대한 부스트와 좋아요를 잃게 되고 원본에 대한 답장은 연결 되지 않습니다.",
@@ -289,6 +296,7 @@
   "emoji_button.search_results": "검색 결과",
   "emoji_button.symbols": "기호",
   "emoji_button.travel": "여행과 장소",
+  "empty_column.account_featured": "목록이 비어있습니다",
   "empty_column.account_hides_collections": "이 사용자는 이 정보를 사용할 수 없도록 설정했습니다",
   "empty_column.account_suspended": "계정 정지됨",
   "empty_column.account_timeline": "이곳에는 게시물이 없습니다!",
@@ -373,6 +381,8 @@
   "generic.saved": "저장됨",
   "getting_started.heading": "시작하기",
   "hashtag.admin_moderation": "#{name}에 대한 중재화면 열기",
+  "hashtag.browse": "#{hashtag}의 게시물 둘러보기",
+  "hashtag.browse_from_account": "@{name}의 #{hashtag} 게시물 둘러보기",
   "hashtag.column_header.tag_mode.all": "및 {additional}",
   "hashtag.column_header.tag_mode.any": "또는 {additional}",
   "hashtag.column_header.tag_mode.none": "{additional}를 제외하고",
@@ -386,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, other {게시물 {counter}개}}",
   "hashtag.counter_by_uses_today": "오늘 {count, plural, other {{counter} 개의 게시물}}",
   "hashtag.follow": "해시태그 팔로우",
+  "hashtag.mute": "#{hashtag} 뮤트",
   "hashtag.unfollow": "해시태그 팔로우 해제",
   "hashtags.and_other": "…및 {count, plural,other {#개}}",
   "hints.profiles.followers_may_be_missing": "이 프로필의 팔로워 목록은 일부 누락되었을 수 있습니다.",
@@ -415,6 +426,7 @@
   "ignore_notifications_modal.not_following_title": "내가 팔로우하지 않는 사람들의 알림을 무시할까요?",
   "ignore_notifications_modal.private_mentions_title": "요청하지 않은 개인 멘션 알림을 무시할까요?",
   "info_button.label": "도움말",
+  "info_button.what_is_alt_text": "<h1>대체 텍스트가 무었인가요?</h1> <p>대체 텍스트는 저시력자, 낮은 인터넷 대역폭 사용자, 더 자세한 문맥을 위해 이미지에 대한 설명을 제공하는 것입니다.</p> <p>깔끔하고 간결하고 객관적인 대체 텍스트를 작성해 모두가 이해하기 쉽게 만들고 접근성이 높아질 수 있습니다.</p><ul><li>중요한 요소에 중점을 두세요</li> <li>이미지 안의 글자를 요약하세요</li> <li>정형화된 문장 구조를 사용하세요</li> <li>중복된 정보를 피하세요</li> <li>복잡한 시각자료(도표나 지도 같은)에선 추세와 주요 결과에 중점을 두세요</li> </ul>",
   "interaction_modal.action.favourite": "계속하려면 내 계정으로 즐겨찾기해야 합니다.",
   "interaction_modal.action.follow": "계속하려면 내 계정으로 팔로우해야 합니다.",
   "interaction_modal.action.reblog": "계속하려면 내 계정으로 리블로그해야 합니다.",
@@ -692,7 +704,7 @@
   "poll_button.remove_poll": "설문 제거",
   "privacy.change": "게시물의 프라이버시 설정을 변경",
   "privacy.direct.long": "이 게시물에서 언급된 모두",
-  "privacy.direct.short": "특정 인물",
+  "privacy.direct.short": "개인 멘션",
   "privacy.private.long": "내 팔로워만",
   "privacy.private.short": "팔로워",
   "privacy.public.long": "마스토돈 내외 모두",
@@ -867,7 +879,9 @@
   "subscribed_languages.target": "{target}에 대한 구독 언어 변경",
   "tabs_bar.home": "홈",
   "tabs_bar.notifications": "알림",
+  "terms_of_service.effective_as_of": "{date}부터 적용됨",
   "terms_of_service.title": "이용 약관",
+  "terms_of_service.upcoming_changes_on": "{date}에 예정된 변경사항",
   "time_remaining.days": "{number} 일 남음",
   "time_remaining.hours": "{number} 시간 남음",
   "time_remaining.minutes": "{number} 분 남음",
@@ -901,5 +915,9 @@
   "video.mute": "음소거",
   "video.pause": "일시정지",
   "video.play": "재생",
-  "video.unmute": "음소거 해제"
+  "video.skip_backward": "뒤로 건너뛰기",
+  "video.skip_forward": "앞으로 건너뛰기",
+  "video.unmute": "음소거 해제",
+  "video.volume_down": "음량 감소",
+  "video.volume_up": "음량 증가"
 }
diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json
index 3e543e938a..23ee9fc932 100644
--- a/app/javascript/mastodon/locales/ku.json
+++ b/app/javascript/mastodon/locales/ku.json
@@ -11,13 +11,16 @@
   "about.not_available": "Ev zanyarî li ser vê rajekarê nehatine peydakirin.",
   "about.powered_by": "Medyaya civakî ya nenavendî bi hêzdariya {mastodon}",
   "about.rules": "Rêbazên rajekar",
+  "account.account_note_header": "Nîşeyên kesane",
   "account.add_or_remove_from_list": "Li lîsteyan zêde bike yan jî rake",
   "account.badges.bot": "Bot",
   "account.badges.group": "Kom",
   "account.block": "@{name} asteng bike",
   "account.block_domain": "Navpera {domain} asteng bike",
+  "account.block_short": "Asteng bike",
   "account.blocked": "Astengkirî",
   "account.cancel_follow_request": "Daxwaza şopandinê vekişîne",
+  "account.copy": "Girêdanê bo profîlê jê bigire",
   "account.direct": "Bi taybetî qale @{name} bike",
   "account.disable_notifications": "Êdî min agahdar neke gava @{name} diweşîne",
   "account.domain_blocked": "Navper hate astengkirin",
@@ -26,11 +29,13 @@
   "account.endorse": "Taybetiyên li ser profîl",
   "account.featured_tags.last_status_at": "Şandiya dawî di {date} de",
   "account.featured_tags.last_status_never": "Şandî tune ne",
-  "account.featured_tags.title": "{name}'s hashtagên taybet",
   "account.follow": "Bişopîne",
+  "account.follow_back": "Bişopîne",
   "account.followers": "Şopîner",
   "account.followers.empty": "Kesekî hin ev bikarhêner neşopandiye.",
+  "account.followers_counter": "{count, plural, one {{counter} şopîner} other {{counter} şopîner}}",
   "account.following": "Dişopîne",
+  "account.following_counter": "{count, plural, one {{counter} dişopîne} other {{counter} dişopîne}}",
   "account.follows.empty": "Ev bikarhêner hin kesekî heya niha neşopandiye.",
   "account.go_to_profile": "Biçe bo profîlê",
   "account.hide_reblogs": "Bilindkirinên ji @{name} veşêre",
@@ -43,7 +48,11 @@
   "account.mention": "Qal @{name} bike",
   "account.moved_to": "{name} diyar kir ku ajimêra nû ya wan niha ev e:",
   "account.mute": "@{name} bêdeng bike",
+  "account.mute_notifications_short": "Agahdariyan bêdeng bike",
+  "account.mute_short": "Bêdeng bike",
   "account.muted": "Bêdengkirî",
+  "account.mutual": "Hevpar",
+  "account.no_bio": "Ti danasîn nehatiye tevlîkirin.",
   "account.open_original_page": "Rûpela resen veke",
   "account.posts": "Şandî",
   "account.posts_with_replies": "Şandî û bersiv",
@@ -52,12 +61,14 @@
   "account.requested_follow": "{name} dixwaze te bişopîne",
   "account.share": "Profîla @{name} parve bike",
   "account.show_reblogs": "Bilindkirinên ji @{name} nîşan bike",
+  "account.statuses_counter": "{count, plural,one {{counter} şandî}other {{counter} şandî}}",
   "account.unblock": "Astengê li ser @{name} rake",
   "account.unblock_domain": "Astengê li ser navperê {domain} rake",
   "account.unblock_short": "Astengiyê rake",
   "account.unendorse": "Li ser profîl nîşan neke",
   "account.unfollow": "Neşopîne",
   "account.unmute": "@{name} bêdeng neke",
+  "account.unmute_notifications_short": "Agahdariyan bêdeng bike",
   "account.unmute_short": "Bêdeng neke",
   "account_note.placeholder": "Bitikîne bo nîşeyekê tevlî bikî",
   "admin.dashboard.daily_retention": "Rêjeya ragirtina bikarhêner bi roj piştî tomarkirinê",
@@ -69,9 +80,18 @@
   "alert.rate_limited.title": "Rêje sînorkirî ye",
   "alert.unexpected.message": "Çewtiyeke bêhêvî çê bû.",
   "alert.unexpected.title": "Wey li min!",
+  "alt_text_modal.cancel": "Têk bibe",
+  "alt_text_modal.change_thumbnail": "Wêneyê biçûk biguherîne",
+  "alt_text_modal.done": "Qediya",
   "announcement.announcement": "Daxuyanî",
+  "annual_report.summary.followers.followers": "şopîner",
+  "annual_report.summary.followers.total": "{count} tevahî",
+  "annual_report.summary.new_posts.new_posts": "şandiyên nû",
   "attachments_list.unprocessed": "(bêpêvajo)",
   "audio.hide": "Dengê veşêre",
+  "block_modal.show_less": "Kêmtir nîşan bide",
+  "block_modal.show_more": "Bêtir nîşan bide",
+  "block_modal.title": "Bikarhêner asteng bike?",
   "boost_modal.combo": "Ji bo derbas bî carekî din de pêlê {combo} bike",
   "bundle_column_error.copy_stacktrace": "Rapora çewtiyê jê bigire",
   "bundle_column_error.error.body": "Rûpela xwestî nehate pêşkêşkirin. Dibe ku ew ji ber şaşetiyeke koda me, an jî pirsgirêkeke lihevhatina gerokê be.",
@@ -141,10 +161,12 @@
   "confirmations.logout.message": "Ma tu dixwazî ku derkevî?",
   "confirmations.mute.confirm": "Bêdeng bike",
   "confirmations.redraft.confirm": "Jê bibe & ji nû ve serrast bike",
+  "confirmations.redraft.message": "Bi rastî tu dixwazî şandî ye jê bibî û ji nû ve reşnivîsek çê bikî? Bijarte û şandî wê wenda bibin û bersivên ji bo şandiyê resen wê sêwî bimînin.",
   "confirmations.reply.confirm": "Bersivê bide",
   "confirmations.reply.message": "Bersiva niha li ser peyama ku tu niha berhev dikî dê binivsîne. Ma pê bawer î ku tu dixwazî bidomînî?",
   "confirmations.unfollow.confirm": "Neşopîne",
   "confirmations.unfollow.message": "Ma tu dixwazî ku dev ji şopa {name} berdî?",
+  "content_warning.show_more": "Bêtir nîşan bide",
   "conversation.delete": "Axaftinê jê bibe",
   "conversation.mark_as_read": "Wekî xwendî nîşan bide",
   "conversation.open": "Axaftinê nîşan bide",
@@ -158,6 +180,9 @@
   "disabled_account_banner.text": "Ajimêrê te {disabledAccount} niha neçalak e.",
   "dismissable_banner.community_timeline": "Ev şandiyên giştî yên herî dawî ji kesên ku ajimêrê wan ji aliyê {domain} ve têne pêşkêşkirin.",
   "dismissable_banner.dismiss": "Paşguh bike",
+  "domain_block_modal.block": "Rajekar asteng bike",
+  "domain_pill.server": "Rajekar",
+  "domain_pill.username": "Navê bikarhêner",
   "embed.instructions": "Bi jêgirtina koda jêrîn vê şandiyê li ser malpera xwe bi cih bike.",
   "embed.preview": "Ew ê çawa xuya bibe li vir tê nîşandan:",
   "emoji_button.activity": "Çalakî",
@@ -523,8 +548,6 @@
   "video.expand": "Vîdyoyê berferh bike",
   "video.fullscreen": "Dimendera tijî",
   "video.hide": "Vîdyo veşêre",
-  "video.mute": "Dengê qut bike",
   "video.pause": "Rawestîne",
-  "video.play": "Vêxe",
-  "video.unmute": "Dengê qut neke"
+  "video.play": "Vêxe"
 }
diff --git a/app/javascript/mastodon/locales/kw.json b/app/javascript/mastodon/locales/kw.json
index 8197f01c66..2cd7a3eb0e 100644
--- a/app/javascript/mastodon/locales/kw.json
+++ b/app/javascript/mastodon/locales/kw.json
@@ -339,8 +339,6 @@
   "video.expand": "Efani gwydhyow",
   "video.fullscreen": "Skrin leun",
   "video.hide": "Kudha gwydhyow",
-  "video.mute": "Tawhe son",
   "video.pause": "Powes",
-  "video.play": "Seni",
-  "video.unmute": "Antawhe son"
+  "video.play": "Seni"
 }
diff --git a/app/javascript/mastodon/locales/la.json b/app/javascript/mastodon/locales/la.json
index 80f9a5a592..c6e5d85c07 100644
--- a/app/javascript/mastodon/locales/la.json
+++ b/app/javascript/mastodon/locales/la.json
@@ -23,7 +23,6 @@
   "account.domain_blocked": "Dominium impeditum",
   "account.edit_profile": "Recolere notionem",
   "account.featured_tags.last_status_never": "Nulla contributa",
-  "account.featured_tags.title": "Hashtag notātī {name}",
   "account.followers_counter": "{count, plural, one {{counter} sectator} other {{counter} sectatores}}",
   "account.following_counter": "{count, plural, one {{counter} sectans} other {{counter} sectans}}",
   "account.moved_to": "{name} significavit eum suam rationem novam nunc esse:",
@@ -241,6 +240,5 @@
   "units.short.thousand": "{count} millia",
   "upload_button.label": "Imaginēs, vīdeō aut fīle audītūs adde",
   "upload_form.edit": "Recolere",
-  "upload_progress.label": "Uploading…",
-  "video.mute": "Confutare soni"
+  "upload_progress.label": "Uploading…"
 }
diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json
index ed91aaccfe..f67ef676ad 100644
--- a/app/javascript/mastodon/locales/lad.json
+++ b/app/javascript/mastodon/locales/lad.json
@@ -29,7 +29,6 @@
   "account.endorse": "Avalia en profil",
   "account.featured_tags.last_status_at": "Ultima publikasyon de {date}",
   "account.featured_tags.last_status_never": "No ay publikasyones",
-  "account.featured_tags.title": "Etiketas avaliadas de {name}",
   "account.follow": "Sige",
   "account.follow_back": "Sige tamyen",
   "account.followers": "Suivantes",
@@ -65,6 +64,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} publikasyon} other {{counter} publikasyones}}",
   "account.unblock": "Dezbloka a @{name}",
   "account.unblock_domain": "Dezbloka domeno {domain}",
+  "account.unblock_domain_short": "Dezbloka",
   "account.unblock_short": "Dezbloka",
   "account.unendorse": "No avalia en profil",
   "account.unfollow": "Desige",
@@ -86,6 +86,10 @@
   "alert.unexpected.message": "Afito un yerro no asperado.",
   "alert.unexpected.title": "Atyo!",
   "alt_text_badge.title": "Teksto alternativo",
+  "alt_text_modal.add_alt_text": "Adjusta teksto alternativo",
+  "alt_text_modal.cancel": "Anula",
+  "alt_text_modal.change_thumbnail": "Troka minyatura",
+  "alt_text_modal.done": "Fecho",
   "announcement.announcement": "Pregon",
   "annual_report.summary.archetype.pollster": "El anketero",
   "annual_report.summary.followers.followers": "suivantes",
@@ -129,9 +133,11 @@
   "column.blocks": "Utilizadores blokados",
   "column.bookmarks": "Markadores",
   "column.community": "Linya lokala",
+  "column.create_list": "Kriya lista",
   "column.direct": "Enmentaduras privadas",
   "column.directory": "Eksplora profiles",
   "column.domain_blocks": "Domenos blokados",
+  "column.edit_list": "Edita lista",
   "column.favourites": "Te plazen",
   "column.firehose": "Linyas en bivo",
   "column.follow_requests": "Solisitudes de segimiento",
@@ -148,6 +154,7 @@
   "column_header.pin": "Fiksa",
   "column_header.show_settings": "Amostra opsyones",
   "column_header.unpin": "Defiksar",
+  "column_search.cancel": "Anula",
   "column_subheading.settings": "Opsyones",
   "community.column_settings.local_only": "Solo lokalas",
   "community.column_settings.media_only": "Solo multimedia",
@@ -189,9 +196,12 @@
   "confirmations.discard_edit_media.message": "Tienes trokamientos no guadrados en la deskripsion o vista previa. Keres efasarlos entanto?",
   "confirmations.edit.confirm": "Edita",
   "confirmations.edit.message": "Si edites agora, kitaras el mesaj kualo estas eskriviendo aktualmente. Estas siguro ke keres fazerlo?",
+  "confirmations.follow_to_list.title": "Segir utilizador?",
   "confirmations.logout.confirm": "Sal",
   "confirmations.logout.message": "Estas siguro ke keres salir de tu kuento?",
   "confirmations.logout.title": "Salir?",
+  "confirmations.missing_alt_text.confirm": "Adjusta teksto alternativo",
+  "confirmations.missing_alt_text.title": "Adjustar teksto alternativo?",
   "confirmations.mute.confirm": "Silensia",
   "confirmations.redraft.confirm": "Efasa i reeskrive",
   "confirmations.redraft.message": "Estas siguro ke keres efasar esta publikasyon i reeskrivirla? Pedreras todos los favoritos i repartajasyones asosiados kon esta publikasyon i repuestas a eya seran guerfanadas.",
@@ -219,6 +229,8 @@
   "disabled_account_banner.text": "Tu kuento {disabledAccount} esta aktualmente inkapasitado.",
   "dismissable_banner.community_timeline": "Estas son las publikasyones publikas mas resientes de las personas kualos kuentos estan balabayados en {domain}.",
   "dismissable_banner.dismiss": "Kita",
+  "dismissable_banner.explore_statuses": "Estas publikasyones del fediverso estan agora popularas. Publikasyones mas muevas, kon mas repartajasiones i favoritadas por mas djente aparesen primero.",
+  "dismissable_banner.public_timeline": "Estas son las publikasyones publikas mas resientes de personas en el fediverso a las kualas la djente de {domain} sige.",
   "domain_block_modal.block": "Bloka sirvidor",
   "domain_block_modal.block_account_instead": "Bloka @{name} en su lugar",
   "domain_block_modal.they_can_interact_with_old_posts": "Las personas de este sirvidor pueden enteraktuar kon tus puvlikasyones viejas.",
@@ -327,6 +339,7 @@
   "footer.status": "Estado",
   "generic.saved": "Guadrado",
   "getting_started.heading": "Primos pasos",
+  "hashtag.admin_moderation": "Avre la enterfaz de moderasyon para #{name}",
   "hashtag.column_header.tag_mode.all": "i {additional}",
   "hashtag.column_header.tag_mode.any": "o {additional}",
   "hashtag.column_header.tag_mode.none": "sin {additional}",
@@ -363,6 +376,7 @@
   "ignore_notifications_modal.not_followers_title": "Inyorar avizos de personas a las kualas no te sigen?",
   "ignore_notifications_modal.not_following_title": "Inyorar avizos de personas a las kualas no siges?",
   "ignore_notifications_modal.private_mentions_title": "Ignorar avizos de mensyones privadas no solisitadas?",
+  "info_button.label": "Ayuda",
   "interaction_modal.on_another_server": "En otro sirvidor",
   "interaction_modal.on_this_server": "En este sirvidor",
   "interaction_modal.title.favourite": "Endika ke te plaze publikasyon de {name}",
@@ -415,11 +429,18 @@
   "link_preview.author": "Publikasyon de {name}",
   "link_preview.more_from_author": "Mas de {name}",
   "link_preview.shares": "{count, plural, one {{counter} publikasyon} other {{counter} publikasyones}}",
+  "lists.add_member": "Adjusta",
+  "lists.add_to_list": "Adjusta a lista",
+  "lists.create": "Kriya",
+  "lists.create_list": "Kriya lista",
   "lists.delete": "Efasa lista",
+  "lists.done": "Fecho",
   "lists.edit": "Edita lista",
   "lists.replies_policy.followed": "Kualseker utilizador segido",
   "lists.replies_policy.list": "Miembros de la lista",
   "lists.replies_policy.none": "Dinguno",
+  "lists.save": "Guadra",
+  "lists.search": "Bushka",
   "load_pending": "{count, plural, one {# muevo elemento} other {# muevos elementos}}",
   "loading_indicator.label": "Eskargando…",
   "media_gallery.hide": "Eskonde",
@@ -544,7 +565,10 @@
   "notifications_permission_banner.enable": "Kapasita avizos de ensimameza",
   "notifications_permission_banner.how_to_control": "Para risivir avizos kuando Mastodon no esta avierto, kapasita avizos de ensimameza. Puedes kontrolar presizamente kualos tipos de enteraksiones djeneren avizos de ensimameza kon el boton {icon} arriva kuando esten kapasitadas.",
   "notifications_permission_banner.title": "Nunkua te piedres niente",
+  "onboarding.follows.back": "Atras",
+  "onboarding.follows.done": "Fecho",
   "onboarding.follows.empty": "Malorozamente, no se pueden amostrar rezultados en este momento. Puedes aprovar uzar la bushkeda o navigar por la pajina de eksplorasyon para topar personas a las que segir, o aprovarlo de muevo mas tadre.",
+  "onboarding.follows.search": "Bushka",
   "onboarding.profile.discoverable": "Faz ke mi profil apareska en bushkedas",
   "onboarding.profile.discoverable_hint": "Kuando permites ke tu profil sea diskuvriravle en Mastodon, tus publikasyones podran apareser en rezultados de bushkedas i trendes i tu profil podra ser sujerido a personas kon intereses similares a los tuyos.",
   "onboarding.profile.display_name": "Nombre amostrado",
@@ -570,7 +594,7 @@
   "poll_button.remove_poll": "Kita anketa",
   "privacy.change": "Troka privasita de publikasyon",
   "privacy.direct.long": "Todos enmentados en la publikasyon",
-  "privacy.direct.short": "Djente espesifika",
+  "privacy.direct.short": "Enmentadura privada",
   "privacy.private.long": "Solo para tus suivantes",
   "privacy.private.short": "Suivantes",
   "privacy.public.long": "Todos en i afuera de Mastodon",
@@ -665,8 +689,10 @@
   "search_results.accounts": "Profiles",
   "search_results.all": "Todos",
   "search_results.hashtags": "Etiketas",
+  "search_results.no_results": "No ay rezultados.",
   "search_results.see_all": "Ve todo",
   "search_results.statuses": "Publikasyones",
+  "search_results.title": "Bushka por \"{q}\"",
   "server_banner.about_active_users": "Utilizadores aktivos en este sirvidor durante los ultimos 30 diyas (utilizadores aktivos mensuales)",
   "server_banner.active_users": "utilizadores aktivos",
   "server_banner.administered_by": "Administrado por:",
@@ -757,8 +783,8 @@
   "video.expand": "Espande video",
   "video.fullscreen": "Ekran kompleto",
   "video.hide": "Eskonde video",
-  "video.mute": "Silensia son",
+  "video.mute": "Silensia",
   "video.pause": "Pauza",
   "video.play": "Reproduze",
-  "video.unmute": "Desilensia son"
+  "video.unmute": "Desilensia"
 }
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 083527f7d5..7110e809c1 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -29,7 +29,6 @@
   "account.endorse": "Rodyti profilyje",
   "account.featured_tags.last_status_at": "Paskutinis įrašas {date}",
   "account.featured_tags.last_status_never": "Nėra įrašų",
-  "account.featured_tags.title": "{name} rodomi saitažodžiai",
   "account.follow": "Sekti",
   "account.follow_back": "Sekti atgal",
   "account.followers": "Sekėjai",
@@ -218,6 +217,10 @@
   "confirmations.logout.confirm": "Atsijungti",
   "confirmations.logout.message": "Ar tikrai nori atsijungti?",
   "confirmations.logout.title": "Atsijungti?",
+  "confirmations.missing_alt_text.confirm": "Pridėti alternatyvųjį tekstą",
+  "confirmations.missing_alt_text.message": "Jūsų įrašas turi mediją be alternatyvaus teksto. Pridėjus aprašymus, jūsų turinys taps pasiekiamas daugeliui asmenų.",
+  "confirmations.missing_alt_text.secondary": "Siųsti vis tiek",
+  "confirmations.missing_alt_text.title": "Pridėti alternatyvųjį tekstą?",
   "confirmations.mute.confirm": "Nutildyti",
   "confirmations.redraft.confirm": "Ištrinti ir iš naujo parengti",
   "confirmations.redraft.message": "Ar tikrai nori ištrinti šį įrašą ir parengti jį iš naujo? Bus prarasti mėgstami ir pasidalinimai, o atsakymai į originalų įrašą bus panaikinti.",
@@ -680,7 +683,7 @@
   "poll_button.remove_poll": "Pašalinti apklausą",
   "privacy.change": "Keisti įrašo privatumą",
   "privacy.direct.long": "Visus, paminėtus įraše",
-  "privacy.direct.short": "Konkretūs žmonės",
+  "privacy.direct.short": "Privatus paminėjimas",
   "privacy.private.long": "Tik sekėjams",
   "privacy.private.short": "Sekėjai",
   "privacy.public.long": "Bet kas iš Mastodon ir ne Mastodon",
@@ -853,7 +856,9 @@
   "subscribed_languages.target": "Keisti prenumeruojamas kalbas {target}",
   "tabs_bar.home": "Pagrindinis",
   "tabs_bar.notifications": "Pranešimai",
+  "terms_of_service.effective_as_of": "Įsigaliojo nuo {date}.",
   "terms_of_service.title": "Paslaugų sąlygos",
+  "terms_of_service.upcoming_changes_on": "Numatomi pakeitimai {date}.",
   "time_remaining.days": "liko {number, plural, one {# diena} few {# dienos} many {# dienos} other {# dienų}}",
   "time_remaining.hours": "liko {number, plural, one {# valanda} few {# valandos} many {# valandos} other {# valandų}}",
   "time_remaining.minutes": "liko {number, plural, one {# minutė} few {# minutės} many {# minutės} other {# minučių}}",
@@ -884,8 +889,6 @@
   "video.expand": "Išplėsti vaizdo įrašą",
   "video.fullscreen": "Visas ekranas",
   "video.hide": "Slėpti vaizdo įrašą",
-  "video.mute": "Išjungti garsą",
   "video.pause": "Pristabdyti",
-  "video.play": "Leisti",
-  "video.unmute": "Įjungti garsą"
+  "video.play": "Leisti"
 }
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 0db471f19a..27760c59ff 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -3,7 +3,7 @@
   "about.contact": "Kontakts:",
   "about.disclaimer": "Mastodon ir bezmaksas atklātā pirmkoda programmatūra un Mastodon gGmbH preču zīme.",
   "about.domain_blocks.no_reason_available": "Iemesls nav norādīts",
-  "about.domain_blocks.preamble": "Mastodon parasti ļauj apskatīt saturu un mijiedarboties ar lietotājiem no jebkura cita federācijas servera. Šie ir izņēmumi, kas veikti šajā konkrētajā serverī.",
+  "about.domain_blocks.preamble": "Mastodon parasti ļauj apskatīt saturu un mijiedarboties ar lietotājiem no jebkura cita fediversa servera. Šie ir izņēmumi, kas veikti tieši šajā serverī.",
   "about.domain_blocks.silenced.explanation": "Parasti tu neredzēsi profilus un saturu no šī servera, ja vien tu nepārprotami izvēlēsies to pārskatīt vai sekot.",
   "about.domain_blocks.silenced.title": "Ierobežotie",
   "about.domain_blocks.suspended.explanation": "Nekādi dati no šī servera netiks apstrādāti, uzglabāti vai apmainīti, padarot neiespējamu mijiedarbību vai saziņu ar lietotājiem no šī servera.",
@@ -29,7 +29,6 @@
   "account.endorse": "Izcelts profilā",
   "account.featured_tags.last_status_at": "Beidzamā ziņa {date}",
   "account.featured_tags.last_status_never": "Ierakstu nav",
-  "account.featured_tags.title": "{name} izceltie tēmturi",
   "account.follow": "Sekot",
   "account.follow_back": "Sekot atpakaļ",
   "account.followers": "Sekotāji",
@@ -54,7 +53,7 @@
   "account.muted": "Apklusināts",
   "account.mutual": "Abpusēji",
   "account.no_bio": "Apraksts nav sniegts.",
-  "account.open_original_page": "Atvērt oriģinālo lapu",
+  "account.open_original_page": "Atvērt pirmavota lapu",
   "account.posts": "Ieraksti",
   "account.posts_with_replies": "Ieraksti un atbildes",
   "account.report": "Sūdzēties par @{name}",
@@ -86,6 +85,12 @@
   "alert.unexpected.message": "Radās negaidīta kļūda.",
   "alert.unexpected.title": "Ups!",
   "alt_text_badge.title": "Alt teksts",
+  "alt_text_modal.add_text_from_image": "Pievienot tekstu no attēla",
+  "alt_text_modal.cancel": "Atcelt",
+  "alt_text_modal.change_thumbnail": "Nomainīt sīktēlu",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Aprakstīt šo cilvēkiem ar dzirdes traucējumiem…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Aprakstīt šo cilvēkiem ar redzes traucējumiem…",
+  "alt_text_modal.done": "Gatavs",
   "announcement.announcement": "Paziņojums",
   "annual_report.summary.archetype.oracle": "Orākuls",
   "annual_report.summary.archetype.replier": "Sabiedriskais tauriņš",
@@ -167,7 +172,7 @@
   "compose.published.open": "Atvērt",
   "compose.saved.body": "Ziņa saglabāta.",
   "compose_form.direct_message_warning_learn_more": "Uzzināt vairāk",
-  "compose_form.encryption_warning": "Mastodon ieraksti nav pilnībā šifrēti. Nedalies ar jebkādu jutīgu informāciju caur Mastodon!",
+  "compose_form.encryption_warning": "Mastodon ieraksti nav pilnībā šifrēti. Nedalies ar jebkādu jūtīgu informāciju caur Mastodon!",
   "compose_form.hashtag_warning": "Šis ieraksts netiks uzrādīts nevienā tēmturī, jo tas nav redzams visiem. Tikai visiem redzamos ierakstus var meklēt pēc tēmtura.",
   "compose_form.lock_disclaimer": "Tavs konts nav {locked}. Ikviens var Tev sekot, lai redzētu tikai sekotājiem paredzētos ierakstus.",
   "compose_form.lock_disclaimer.lock": "slēgts",
@@ -205,9 +210,10 @@
   "confirmations.logout.confirm": "Iziet",
   "confirmations.logout.message": "Vai tiešām vēlies izrakstīties?",
   "confirmations.logout.title": "Atteikties?",
+  "confirmations.missing_alt_text.secondary": "Vienalga iesūtīt",
   "confirmations.mute.confirm": "Apklusināt",
   "confirmations.redraft.confirm": "Dzēst un pārrakstīt",
-  "confirmations.redraft.message": "Vai tiešām vēlies dzēst šo ziņu un no jauna noformēt to? Izlase un pastiprinājumi tiks zaudēti, un atbildes uz sākotnējo ziņu tiks atstātas bez autoratlīdzības.",
+  "confirmations.redraft.message": "Vai tiešām vēlies izdzēst šo ierakstu un veidot jaunu tā uzmetumu? Pievienošana izlasēs un pastiprinājumi tiks zaudēti, un sākotnējā ieraksta atbildes paliks bez saiknes ar to.",
   "confirmations.redraft.title": "Dzēst un rakstīt vēlreiz?",
   "confirmations.reply.confirm": "Atbildēt",
   "confirmations.reply.message": "Tūlītēja atbildēšana pārrakstīs pašlaik sastādīto ziņu. Vai tiešām turpināt?",
@@ -225,7 +231,7 @@
   "copy_icon_button.copied": "Ievietots starpliktuvē",
   "copypaste.copied": "Nokopēts",
   "copypaste.copy_to_clipboard": "Kopēt uz starpliktuvi",
-  "directory.federated": "No pazīstamas federācijas",
+  "directory.federated": "No zināma fediversa",
   "directory.local": "Tikai no {domain}",
   "directory.new_arrivals": "Jaunpienācēji",
   "directory.recently_active": "Nesen aktīvi",
@@ -304,6 +310,7 @@
   "filter_modal.select_filter.subtitle": "Izmanto esošu kategoriju vai izveido jaunu",
   "filter_modal.select_filter.title": "Filtrēt šo ziņu",
   "filter_modal.title.status": "Filtrēt ziņu",
+  "filtered_notifications_banner.title": "Filtrētie paziņojumi",
   "firehose.all": "Visi",
   "firehose.local": "Šis serveris",
   "firehose.remote": "Citi serveri",
@@ -314,6 +321,8 @@
   "follow_suggestions.dismiss": "Vairs nerādīt",
   "follow_suggestions.friends_of_friends_longer": "Populārs to cilvēku vidū, kuriem tu seko",
   "follow_suggestions.personalized_suggestion": "Pielāgots ieteikums",
+  "follow_suggestions.popular_suggestion": "Populārs ieteikums",
+  "follow_suggestions.popular_suggestion_longer": "Populārs {domain}",
   "follow_suggestions.similar_to_recently_followed_longer": "Līdzīgi profieliem, kuriem nesen sāki sekot",
   "follow_suggestions.view_all": "Skatīt visu",
   "follow_suggestions.who_to_follow": "Kam sekot",
@@ -325,8 +334,10 @@
   "footer.privacy_policy": "Privātuma politika",
   "footer.source_code": "Skatīt pirmkodu",
   "footer.status": "Statuss",
+  "footer.terms_of_service": "Pakalpojuma noteikumi",
   "generic.saved": "Saglabāts",
   "getting_started.heading": "Darba sākšana",
+  "hashtag.admin_moderation": "Atvērt #{name} satura pārraudzības saskarni",
   "hashtag.column_header.tag_mode.all": "un {additional}",
   "hashtag.column_header.tag_mode.any": "vai {additional}",
   "hashtag.column_header.tag_mode.none": "bez {additional}",
@@ -462,9 +473,9 @@
   "notification.moderation_warning": "Ir saņemts satura pārraudzības brīdinājums",
   "notification.moderation_warning.action_delete_statuses": "Daži no Taviem ierakstiem tika noņemti.",
   "notification.moderation_warning.action_disable": "Tavs konts tika atspējots.",
-  "notification.moderation_warning.action_mark_statuses_as_sensitive": "Daži no Taviem ierakstiem tika atzīmēti kā jutīgi.",
+  "notification.moderation_warning.action_mark_statuses_as_sensitive": "Daži no Taviem ierakstiem tika atzīmēti kā jūtīgi.",
   "notification.moderation_warning.action_none": "Konts ir saņēmis satura pārraudzības brīdinājumu.",
-  "notification.moderation_warning.action_sensitive": "Tavi ieraksti turpmāk tiks atzīmēti kā jutīgi.",
+  "notification.moderation_warning.action_sensitive": "Tavi ieraksti turpmāk tiks atzīmēti kā jūtīgi.",
   "notification.moderation_warning.action_silence": "Tavs konts tika ierobežots.",
   "notification.moderation_warning.action_suspend": "Tava konta darbība tika apturēta.",
   "notification.own_poll": "Tava aptauja ir noslēgusies",
@@ -489,6 +500,7 @@
   "notifications.column_settings.filter_bar.category": "Atrās atlasīšanas josla",
   "notifications.column_settings.follow": "Jauni sekotāji:",
   "notifications.column_settings.follow_request": "Jauni sekošanas pieprasījumi:",
+  "notifications.column_settings.group": "Grupēt",
   "notifications.column_settings.mention": "Pieminēšanas:",
   "notifications.column_settings.poll": "Aptaujas rezultāti:",
   "notifications.column_settings.push": "Uznirstošie paziņojumi",
@@ -513,13 +525,15 @@
   "notifications.permission_denied_alert": "Darbvirsmas paziņojumus nevar iespējot, jo pārlūkprogrammai atļauja tika iepriekš atteikta",
   "notifications.permission_required": "Darbvirsmas paziņojumi nav pieejami, jo nav piešķirta nepieciešamā atļauja.",
   "notifications.policy.accept": "Pieņemt",
+  "notifications.policy.drop": "Ignorēt",
   "notifications.policy.filter_new_accounts_title": "Jauni konti",
   "notifications.policy.filter_not_followers_title": "Cilvēki, kuri Tev neseko",
   "notifications.policy.filter_not_following_hint": "Līdz tos pašrocīgi apstiprināsi",
   "notifications.policy.filter_not_following_title": "Cilvēki, kuriem Tu neseko",
   "notifications_permission_banner.enable": "Iespējot darbvirsmas paziņojumus",
-  "notifications_permission_banner.how_to_control": "Lai saņemtu paziņojumus, kad Mastodon nav atvērts, iespējo darbvirsmas paziņojumus. Vari precīzi kontrolēt, kāda veida mijiedarbības rada darbvirsmas paziņojumus, izmantojot augstāk redzamo pogu {icon}, kad tie būs iespējoti.",
+  "notifications_permission_banner.how_to_control": "Lai saņemtu paziņojumus, kad Mastodon nav atvērts, jāiespējo darbvirsmas paziņojumi. Var pārvaldīt, tieši kāda veida mijiedarbības rada darbvirsmas paziņojumus, izmantojot augstāk redzamo pogu {icon}, tiklīdz tie būs iespējoti.",
   "notifications_permission_banner.title": "Nekad nepalaid neko garām",
+  "onboarding.follows.back": "Atpakaļ",
   "onboarding.follows.empty": "Diemžēl pašlaik nevar parādīt rezultātus. Vari mēģināt izmantot meklēšanu vai pārlūkot izpētes lapu, lai atrastu cilvēkus, kuriem sekot, vai vēlāk mēģināt vēlreiz.",
   "onboarding.profile.discoverable": "Padarīt manu profilu atklājamu",
   "onboarding.profile.display_name": "Attēlojamais vārds",
@@ -545,7 +559,6 @@
   "poll_button.remove_poll": "Noņemt aptauju",
   "privacy.change": "Mainīt ieraksta privātumu",
   "privacy.direct.long": "Visi ierakstā pieminētie",
-  "privacy.direct.short": "Noteikti cilvēki",
   "privacy.private.long": "Tikai Tavi sekotāji",
   "privacy.private.short": "Sekotāji",
   "privacy.public.long": "Jebkurš Mastodon un ārpus tā",
@@ -583,7 +596,7 @@
   "report.close": "Darīts",
   "report.comment.title": "Vai, tavuprāt, mums vēl būtu kas jāzina?",
   "report.forward": "Pārsūtīt {target}",
-  "report.forward_hint": "Konts ir no cita servera. Vai nosūtīt anonimizētu sūdzības kopiju arī tam?",
+  "report.forward_hint": "Konts ir no cita servera. Vai nosūtīt anonimizētu ziņojuma kopiju arī tur?",
   "report.mute": "Apklusināt",
   "report.mute_explanation": "Tu neredzēsi viņu ierakstus. Viņi joprojām var Tev sekot un redzēt Tavus ierakstus un nezinās, ka viņi ir apklusināti.",
   "report.next": "Tālāk",
@@ -675,10 +688,10 @@
   "status.mute_conversation": "Apklusināt sarunu",
   "status.open": "Paplašināt šo ziņu",
   "status.pin": "Piespraust profilam",
-  "status.pinned": "Piespraustais ieraksts",
+  "status.pinned": "Piesprausts ieraksts",
   "status.read_more": "Lasīt vairāk",
   "status.reblog": "Pastiprināt",
-  "status.reblog_private": "Pastiprināt, nemainot redzamību",
+  "status.reblog_private": "Pastiprināt ar sākotnējo redzamību",
   "status.reblogged_by": "{name} pastiprināja",
   "status.reblogs": "{count, plural, zero {pastiprinājumu} one {pastiprinājums} other {pastiprinājumi}}",
   "status.reblogs.empty": "Neviens šo ierakstu vēl nav pastiprinājis. Kad būs, tie parādīsies šeit.",
@@ -688,11 +701,11 @@
   "status.reply": "Atbildēt",
   "status.replyAll": "Atbildēt uz tematu",
   "status.report": "Ziņot par @{name}",
-  "status.sensitive_warning": "Sensitīvs saturs",
+  "status.sensitive_warning": "Jūtīgs saturs",
   "status.share": "Kopīgot",
   "status.show_less_all": "Rādīt mazāk visiem",
   "status.show_more_all": "Rādīt vairāk visiem",
-  "status.show_original": "Rādīt oriģinālu",
+  "status.show_original": "Rādīt pirmavotu",
   "status.title.with_attachments": "{user} publicējis {attachmentCount, plural, one {pielikumu} other {{attachmentCount} pielikumus}}",
   "status.translate": "Tulkot",
   "status.translated_from_with": "Tulkots no {lang} izmantojot {provider}",
@@ -729,8 +742,6 @@
   "video.expand": "Paplašināt video",
   "video.fullscreen": "Pilnekrāns",
   "video.hide": "Slēpt video",
-  "video.mute": "Izslēgt skaņu",
   "video.pause": "Pauze",
-  "video.play": "Atskaņot",
-  "video.unmute": "Ieslēgt skaņu"
+  "video.play": "Atskaņot"
 }
diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json
index 95da7b9970..b865f1aa6e 100644
--- a/app/javascript/mastodon/locales/ml.json
+++ b/app/javascript/mastodon/locales/ml.json
@@ -405,7 +405,6 @@
   "video.expand": "വീഡിയോ വികസപ്പിക്കൂ",
   "video.fullscreen": "പൂർണ്ണ സ്ക്രീൻ",
   "video.hide": "വീഡിയോ മറയ്ക്കുക",
-  "video.mute": "ശബ്ദം നിശബ്‌ദമാക്കൂ",
   "video.pause": "താൽക്കാലികമായി നിർത്തുക",
   "video.play": "പ്ലേ"
 }
diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json
index 94f9aef261..919a34532f 100644
--- a/app/javascript/mastodon/locales/mr.json
+++ b/app/javascript/mastodon/locales/mr.json
@@ -28,7 +28,6 @@
   "account.endorse": "प्रोफाइलवरील वैशिष्ट्य",
   "account.featured_tags.last_status_at": "शेवटचे पोस्ट {date} रोजी",
   "account.featured_tags.last_status_never": "पोस्ट नाहीत",
-  "account.featured_tags.title": "{name} चे वैशिष्ट्यीकृत हॅशटॅग",
   "account.follow": "अनुयायी व्हा",
   "account.follow_back": "आपणही अनुसरण करा",
   "account.followers": "अनुयायी",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index b934c3ea63..483261da6b 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -1,9 +1,9 @@
 {
-  "about.blocks": "Pelayan yang disederhanakan",
+  "about.blocks": "Pelayan yang diselaraskan",
   "about.contact": "Hubungi:",
   "about.disclaimer": "Mastodon ialah perisian sumber terbuka percuma, dan merupakan tanda dagangan Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Sebab tidak tersedia",
-  "about.domain_blocks.preamble": "Secara amnya, Mastodon membenarkan anda melihat kandungan daripada dan berinteraksi dengan pengguna daripada mana-mana pelayan dalam dunia persekutuan. Berikut ialah pengecualian yang telah dibuat pada pelayan ini secara khususnya.",
+  "about.domain_blocks.preamble": "Secara amnya, Mastodon membenarkan anda melihat kandungan pengguna daripada mana-mana pelayan dalam alam bersekutu dan berinteraksi dengan mereka. Berikut ialah pengecualian yang khusus pada pelayan ini.",
   "about.domain_blocks.silenced.explanation": "Secara amnya, anda tidak akan melihat profil dan kandungan daripada pelayan ini, kecuali anda mencarinya secara khusus atau ikut serta dengan mengikutinya.",
   "about.domain_blocks.silenced.title": "Terhad",
   "about.domain_blocks.suspended.explanation": "Tiada data daripada pelayan ini yang akan diproses, disimpan atau ditukar, menjadikan sebarang interaksi atau perhubungan dengan pengguna daripada pelayan ini adalah mustahil.",
@@ -11,15 +11,15 @@
   "about.not_available": "Maklumat ini belum tersedia pada pelayan ini.",
   "about.powered_by": "Media sosial terpencar yang dikuasakan oleh {mastodon}",
   "about.rules": "Peraturan pelayan",
-  "account.account_note_header": "Personal note",
+  "account.account_note_header": "Catatan peribadi",
   "account.add_or_remove_from_list": "Tambah atau Buang dari senarai",
-  "account.badges.bot": "Bot",
+  "account.badges.bot": "Automatik",
   "account.badges.group": "Kumpulan",
   "account.block": "Sekat @{name}",
   "account.block_domain": "Sekat domain {domain}",
-  "account.block_short": "Malay",
+  "account.block_short": "Sekat",
   "account.blocked": "Disekat",
-  "account.cancel_follow_request": "Menarik balik permintaan mengikut",
+  "account.cancel_follow_request": "Batalkan permintaan ikut",
   "account.copy": "Salin pautan ke profil",
   "account.direct": "Sebut secara persendirian @{name}",
   "account.disable_notifications": "Berhenti maklumkan saya apabila @{name} mengirim hantaran",
@@ -29,29 +29,28 @@
   "account.endorse": "Tampilkan di profil",
   "account.featured_tags.last_status_at": "Hantaran terakhir pada {date}",
   "account.featured_tags.last_status_never": "Tiada hantaran",
-  "account.featured_tags.title": "Tanda pagar pilihan {name}",
   "account.follow": "Ikuti",
   "account.follow_back": "Ikut balik",
   "account.followers": "Pengikut",
   "account.followers.empty": "Belum ada yang mengikuti pengguna ini.",
-  "account.followers_counter": "{count, plural, one {{counter} Diikuti} other {{counter} Diikuti}}",
-  "account.following": "Mengikuti",
-  "account.following_counter": "{count, plural, other {{counter} following}}",
+  "account.followers_counter": "{count, plural, one {{counter} pengikut} other {{counter} pengikut}}",
+  "account.following": "Ikutan",
+  "account.following_counter": "{count, plural, other {{counter} ikutan}}",
   "account.follows.empty": "Pengguna ini belum mengikuti sesiapa.",
   "account.go_to_profile": "Pergi ke profil",
   "account.hide_reblogs": "Sembunyikan galakan daripada @{name}",
-  "account.in_memoriam": "Dalam Memoriam.",
-  "account.joined_short": "Menyertai",
-  "account.languages": "Tukar bahasa yang dilanggan",
+  "account.in_memoriam": "Dalam kenangan.",
+  "account.joined_short": "Tarikh penyertaan",
+  "account.languages": "Tukar bahasa langganan",
   "account.link_verified_on": "Pemilikan pautan ini telah disemak pada {date}",
-  "account.locked_info": "Status privasi akaun ini dikunci. Pemiliknya menyaring sendiri siapa yang boleh mengikutinya.",
+  "account.locked_info": "Taraf privasi akaun ini dikunci. Pemiliknya menyaring sendiri siapa yang boleh mengikutinya.",
   "account.media": "Media",
   "account.mention": "Sebut @{name}",
   "account.moved_to": "{name} telah menandakan bahawa akaun baru mereka sekarang ialah:",
-  "account.mute": "Bisukan @{name}",
-  "account.mute_notifications_short": "Redam pemberitahuan",
+  "account.mute": "Redamkan @{name}",
+  "account.mute_notifications_short": "Redamkan pemberitahuan",
   "account.mute_short": "Redam",
-  "account.muted": "Dibisukan",
+  "account.muted": "Diredamkan",
   "account.mutual": "Rakan kongsi",
   "account.no_bio": "Tiada penerangan diberikan.",
   "account.open_original_page": "Buka halaman asal",
@@ -65,12 +64,13 @@
   "account.statuses_counter": "{count, plural, other {{counter} siaran}}",
   "account.unblock": "Nyahsekat @{name}",
   "account.unblock_domain": "Nyahsekat domain {domain}",
+  "account.unblock_domain_short": "Nyahsekat",
   "account.unblock_short": "Nyahsekat",
   "account.unendorse": "Jangan tampilkan di profil",
   "account.unfollow": "Nyahikut",
   "account.unmute": "Nyahbisukan @{name}",
-  "account.unmute_notifications_short": "Nyahredam notifikasi",
-  "account.unmute_short": "Nyahbisu",
+  "account.unmute_notifications_short": "Nyahredamkan pemberitahuan",
+  "account.unmute_short": "Nyahredam",
   "account_note.placeholder": "Klik untuk menambah catatan",
   "admin.dashboard.daily_retention": "Kadar pengekalan pengguna mengikut hari selepas mendaftar",
   "admin.dashboard.monthly_retention": "Kadar pengekalan pengguna mengikut bulan selepas mendaftar",
@@ -85,10 +85,47 @@
   "alert.rate_limited.title": "Kadar terhad",
   "alert.unexpected.message": "Berlaku ralat di luar jangkaan.",
   "alert.unexpected.title": "Alamak!",
+  "alt_text_badge.title": "Teks alternatif",
+  "alt_text_modal.add_alt_text": "Tambah teks alternatif",
+  "alt_text_modal.add_text_from_image": "Tambah teks dari imej",
+  "alt_text_modal.cancel": "Batal",
+  "alt_text_modal.change_thumbnail": "Ubah imej kecil",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Terangkan untuk OKU pendengaran…vorite\n",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Terangkan untuk OKU penglihatan…",
+  "alt_text_modal.done": "Selesai",
   "announcement.announcement": "Pengumuman",
+  "annual_report.summary.archetype.booster": "Si pencapap",
+  "annual_report.summary.archetype.lurker": "Si penghendap",
+  "annual_report.summary.archetype.oracle": "Si penilik",
+  "annual_report.summary.archetype.pollster": "Si peninjau",
+  "annual_report.summary.archetype.replier": "Si peramah",
+  "annual_report.summary.followers.followers": "pengikut",
+  "annual_report.summary.followers.total": "sebanyak {count}",
+  "annual_report.summary.here_it_is": "Ini ulasan {year} anda:",
+  "annual_report.summary.highlighted_post.by_favourites": "hantaran paling disukai ramai",
+  "annual_report.summary.highlighted_post.by_reblogs": "hantaran paling digalak ramai",
+  "annual_report.summary.highlighted_post.by_replies": "hantaran paling dibalas ramai",
+  "annual_report.summary.highlighted_post.possessive": "oleh",
+  "annual_report.summary.most_used_app.most_used_app": "aplikasi paling banyak digunakan",
+  "annual_report.summary.most_used_hashtag.most_used_hashtag": "tanda pagar paling banyak digunakan",
+  "annual_report.summary.most_used_hashtag.none": "Tiada",
+  "annual_report.summary.new_posts.new_posts": "hantaran baharu",
+  "annual_report.summary.percentile.text": "<topLabel>Anda berkedudukan</topLabel><percentage></percentage><bottomLabel> pengguna {domain}.</bottomLabel>",
+  "annual_report.summary.percentile.we_wont_tell_bernie": "Rahsia anda selamat bersama kami. ;)",
+  "annual_report.summary.thanks": "Terima kasih kerana setia bersama Mastodon!",
   "attachments_list.unprocessed": "(belum diproses)",
   "audio.hide": "Sembunyikan audio",
+  "block_modal.remote_users_caveat": "Kami akan meminta pelayan {domain} untuk menghormati keputusan anda. Bagaimanapun, pematuhan tidak dijamin kerana ada pelayan yang mungkin menangani sekatan dengan cara berbeza. Hantaran awam mungkin masih tampak kepada pengguna yang tidak log masuk.",
+  "block_modal.show_less": "Tunjuk kurang",
+  "block_modal.show_more": "Tunjuk lebih",
+  "block_modal.they_cant_mention": "Dia tidak boleh menyebut tentang anda atau mengikut anda.",
+  "block_modal.they_cant_see_posts": "Dia tidak boleh melihat hantaran anda dan sebaliknya.",
+  "block_modal.they_will_know": "Dia boleh lihat bahawa dia disekat.",
+  "block_modal.title": "Sekat pengguna?",
+  "block_modal.you_wont_see_mentions": "Anda tidak akan melihat hantaran yang menyebut tentangnya.",
   "boost_modal.combo": "Anda boleh tekan {combo} untuk melangkauinya pada waktu lain",
+  "boost_modal.reblog": "Galakkan hantaran?",
+  "boost_modal.undo_reblog": "Nyahgalakkan hantaran?",
   "bundle_column_error.copy_stacktrace": "Salin laporan ralat",
   "bundle_column_error.error.body": "Halaman yang diminta gagal dipaparkan. Ini mungkin disebabkan oleh pepijat dalam kod kami, atau masalah keserasian pelayar.",
   "bundle_column_error.error.title": "Alamak!",
@@ -99,6 +136,7 @@
   "bundle_column_error.routing.body": "Halaman tersebut tidak dapat ditemui. Adakah anda pasti URL dalam bar alamat adalah betul?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Tutup",
+  "bundle_modal_error.message": "Terdapat masalah yang dihadapi ketika memuat layar ini.",
   "bundle_modal_error.retry": "Cuba lagi",
   "closed_registrations.other_server_instructions": "Oleh sebab Mastodon terpencar, anda boleh mencipta akaun pada pelayan lain dan masih berinteraksi dengan pelayan ini.",
   "closed_registrations_modal.description": "Mencipta akaun pada {domain} tidak dapat dibuat sekarang, tetapi sila ingat bahawa anda tidak memerlukan akaun khususnya pada {domain} untuk menggunakan Mastodon.",
@@ -106,18 +144,21 @@
   "closed_registrations_modal.preamble": "Mastodon adalah terpencar, oleh itu di mana-mana anda mencipta akaun anda, anda boleh mengikut dan berinteraksi dengan sesiapa pada pelayan ini. Anda juga boleh hos sendiri!",
   "closed_registrations_modal.title": "Mendaftar pada Mastodon",
   "column.about": "Perihal",
-  "column.blocks": "Pengguna yang disekat",
+  "column.blocks": "Pengguna tersekat",
   "column.bookmarks": "Tanda buku",
   "column.community": "Garis masa tempatan",
+  "column.create_list": "Cipta senarai",
   "column.direct": "Sebutan peribadi",
   "column.directory": "Layari profil",
-  "column.domain_blocks": "Domain disekat",
-  "column.favourites": "Kegemaran",
+  "column.domain_blocks": "Domain tersekat",
+  "column.edit_list": "Sunting senarai",
+  "column.favourites": "Sukaan",
   "column.firehose": "Suapan langsung",
   "column.follow_requests": "Permintaan ikutan",
   "column.home": "Laman Utama",
+  "column.list_members": "Urus ahli senarai",
   "column.lists": "Senarai",
-  "column.mutes": "Pengguna yang dibisukan",
+  "column.mutes": "Pengguna teredam",
   "column.notifications": "Pemberitahuan",
   "column.pins": "Hantaran disemat",
   "column.public": "Garis masa bersekutu",
@@ -128,6 +169,7 @@
   "column_header.pin": "Sematkan",
   "column_header.show_settings": "Tunjukkan tetapan",
   "column_header.unpin": "Nyahsemat",
+  "column_search.cancel": "Batal",
   "column_subheading.settings": "Tetapan",
   "community.column_settings.local_only": "Tempatan sahaja",
   "community.column_settings.media_only": "Media sahaja",
@@ -146,6 +188,7 @@
   "compose_form.poll.duration": "Tempoh undian",
   "compose_form.poll.multiple": "Pelbagai pilihan",
   "compose_form.poll.option_placeholder": "Pilihan {number}",
+  "compose_form.poll.single": "Satu pilihan",
   "compose_form.poll.switch_to_multiple": "Ubah kepada membenarkan aneka undian",
   "compose_form.poll.switch_to_single": "Ubah kepada undian pilihan tunggal",
   "compose_form.poll.type": "Gaya",
@@ -160,21 +203,38 @@
   "confirmations.block.confirm": "Sekat",
   "confirmations.delete.confirm": "Padam",
   "confirmations.delete.message": "Adakah anda pasti anda ingin memadam hantaran ini?",
+  "confirmations.delete.title": "Padam hantaran?",
   "confirmations.delete_list.confirm": "Padam",
   "confirmations.delete_list.message": "Adakah anda pasti anda ingin memadam senarai ini secara kekal?",
+  "confirmations.delete_list.title": "Padam senarai?",
   "confirmations.discard_edit_media.confirm": "Singkir",
   "confirmations.discard_edit_media.message": "Anda belum menyimpan perubahan pada penerangan atau pratonton media. Anda ingin membuangnya?",
   "confirmations.edit.confirm": "Sunting",
   "confirmations.edit.message": "Mengedit sekarang akan menimpa mesej yang sedang anda karang. Adakah anda pasti mahu meneruskan?",
+  "confirmations.edit.title": "Tulis ganti hantaran?",
+  "confirmations.follow_to_list.confirm": "Ikut dan tambah ke senarai",
+  "confirmations.follow_to_list.message": "Anda mesti mengikuti {name} untuk tambahkannya ke senarai.",
+  "confirmations.follow_to_list.title": "Ikut pengguna?",
   "confirmations.logout.confirm": "Log keluar",
   "confirmations.logout.message": "Adakah anda pasti anda ingin log keluar?",
-  "confirmations.mute.confirm": "Bisukan",
+  "confirmations.logout.title": "Log keluar?",
+  "confirmations.missing_alt_text.confirm": "Tambah teks alternatif",
+  "confirmations.missing_alt_text.message": "Hantaran anda mempunyai media tanpa teks alternatif. Kandungan anda akan lebih mudah tercapai jika anda menambah keterangan.",
+  "confirmations.missing_alt_text.secondary": "Hantar saja",
+  "confirmations.missing_alt_text.title": "Tambah teks alternatif?",
+  "confirmations.mute.confirm": "Redamkan",
   "confirmations.redraft.confirm": "Padam & rangka semula",
-  "confirmations.redraft.message": "Adakah anda pasti anda ingin memadam pos ini dan merangkanya semula? Kegemaran dan galakan akan hilang, dan balasan ke pos asal akan menjadi yatim.",
+  "confirmations.redraft.message": "Adakah anda pasti anda ingin memadam hantaran ini dan gubal semula? Sukaan dan galakan akan hilang, dan balasan ke hantaran asal akan menjadi yatim.",
+  "confirmations.redraft.title": "Padam & gubah semula hantaran?",
   "confirmations.reply.confirm": "Balas",
   "confirmations.reply.message": "Membalas sekarang akan menulis ganti mesej yang anda sedang karang. Adakah anda pasti anda ingin teruskan?",
+  "confirmations.reply.title": "Tulis ganti hantaran?",
   "confirmations.unfollow.confirm": "Nyahikut",
   "confirmations.unfollow.message": "Adakah anda pasti anda ingin nyahikuti {name}?",
+  "confirmations.unfollow.title": "Berhenti mengikut pengguna?",
+  "content_warning.hide": "Sorok hantaran",
+  "content_warning.show": "Tunjuk saja",
+  "content_warning.show_more": "Tunjuk lebih",
   "conversation.delete": "Padam perbualan",
   "conversation.mark_as_read": "Tanda sudah dibaca",
   "conversation.open": "Lihat perbualan",
@@ -182,7 +242,7 @@
   "copy_icon_button.copied": "Disalin ke papan klip",
   "copypaste.copied": "Disalin",
   "copypaste.copy_to_clipboard": "Salin ke papan klip",
-  "directory.federated": "Dari fediverse yang diketahui",
+  "directory.federated": "Dari alam bersekutu yang diketahui",
   "directory.local": "Dari {domain} sahaja",
   "directory.new_arrivals": "Ketibaan baharu",
   "directory.recently_active": "Aktif baru-baru ini",
@@ -190,6 +250,23 @@
   "disabled_account_banner.text": "Akaun anda {disabledAccount} telah dinyahaktif.",
   "dismissable_banner.community_timeline": "Inilah hantaran awam terkini daripada orang yang akaun dihos oleh {domain}.",
   "dismissable_banner.dismiss": "Ketepikan",
+  "dismissable_banner.explore_statuses": "Hantaran-hantaran dari seluruh alam bersekutu ini sedang sohor. Hantaran terbaharu dengan lebih banyak galakan dan sukaan diberi kedudukan lebih tinggi.",
+  "dismissable_banner.public_timeline": "Hantaran-hantaran awam terkini ini dari pengguna alam bersekutu yang diikuti oleh pengguna dari {domain}.",
+  "domain_block_modal.block": "Sekat pelayan",
+  "domain_block_modal.block_account_instead": "Sekat @{name} sahaja",
+  "domain_block_modal.they_can_interact_with_old_posts": "Pengguna dari pelayan ini boleh berinteraksi dengan hantaran lama anda.",
+  "domain_block_modal.they_cant_follow": "Pengguna dari pelayan ini tidak boleh mengikuti anda.",
+  "domain_block_modal.they_wont_know": "Dia tidak akan tahu bahawa dia telah disekat.",
+  "domain_block_modal.title": "Sekat domain?",
+  "domain_block_modal.you_will_lose_num_followers": "Anda akan kehilangan {followersCount, plural, other {{followersCountDisplay} pengikut}} dan {followingCount, plural, other {{followingCountDisplay} ikutan}}.",
+  "domain_block_modal.you_will_lose_relationships": "Anda akan kehilangan semua pengikut dan ikutan anda dari pelayan ini.",
+  "domain_block_modal.you_wont_see_posts": "Anda tidak akan melihat hantaran atau pemberitahuan dari pengguna pada pelayan ini.",
+  "domain_pill.activitypub_lets_connect": "Hal ini membolehkan anda berhubung dan berinteraksi bukan sahaja dengan pengguna Mastodon tetapi melintasi pelbagai aplikasi sosial juga.",
+  "domain_pill.activitypub_like_language": "ActivityPub adalah seperti bahasa yang digunakan oleh Mastodon untuk berhubung dengan jaringan sosial lain.",
+  "domain_pill.server": "Pelayan",
+  "domain_pill.your_handle": "Pemegang anda:",
+  "domain_pill.your_server": "Rumah maya anda, tempatnya hantaran anda disimpan. Tidak berkenan dengan yang ini? Pindah antara pelayan pada bila-bila masa dan bawa pengikut anda sekali.",
+  "domain_pill.your_username": "Pengenal unik anda pada pelayan ini. Anda mungkin akan berkongsi nama pengguna dengan pengguna daripada pelayan lain.",
   "embed.instructions": "Benam hantaran ini di laman sesawang anda dengan menyalin kod berikut.",
   "embed.preview": "Begini rupanya nanti:",
   "emoji_button.activity": "Aktiviti",
@@ -207,6 +284,7 @@
   "emoji_button.search_results": "Hasil carian",
   "emoji_button.symbols": "Simbol",
   "emoji_button.travel": "Kembara & Tempat",
+  "empty_column.account_featured": "Senarai ini kosong",
   "empty_column.account_hides_collections": "Pengguna ini telah memilih untuk tidak menyediakan informasi tersebut",
   "empty_column.account_suspended": "Akaun digantung",
   "empty_column.account_timeline": "Tiada hantaran di sini!",
@@ -217,14 +295,14 @@
   "empty_column.direct": "Anda belum mempunyai sebarang sebutan peribadi lagi. Apabila anda menghantar atau menerima satu, ia akan dipaparkan di sini.",
   "empty_column.domain_blocks": "Belum ada domain yang disekat.",
   "empty_column.explore_statuses": "Tiada apa-apa yang sohor kini sekarang. Semaklah kemudian!",
-  "empty_column.favourited_statuses": "Anda belum mempunyai sebarang pos kegemaran. Apabila anda kegemaran, ia akan dipaparkan di sini.",
-  "empty_column.favourites": "Tiada siapa yang menggemari pos ini lagi. Apabila seseorang melakukannya, mereka akan muncul di sini.",
+  "empty_column.favourited_statuses": "Anda belum mempunyai sebarang hantaran sukaan lagi. Hantaran akan muncul di sini apabila disukai oleh anda.",
+  "empty_column.favourites": "Hantaran ini belum disukai mana-mana pengguna lagi. Pengguna yang menyukai akan muncul di sini.",
   "empty_column.follow_requests": "Anda belum mempunyai permintaan ikutan. Ia akan terpapar di sini apabila ada nanti.",
   "empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
   "empty_column.hashtag": "Belum ada apa-apa dengan tanda pagar ini.",
   "empty_column.home": "Garis masa laman utama anda kosong! Ikuti lebih ramai orang untuk mengisinya. {suggestions}",
   "empty_column.list": "Tiada apa-apa di senarai ini lagi. Apabila ahli senarai ini menerbitkan hantaran baharu, ia akan dipaparkan di sini.",
-  "empty_column.mutes": "Anda belum membisukan sesiapa.",
+  "empty_column.mutes": "Anda belum meredamkan sesiapa.",
   "empty_column.notifications": "Anda belum ada sebarang pemberitahuan. Apabila orang lain berinteraksi dengan anda, ia akan muncul di sini.",
   "empty_column.public": "Tiada apa-apa di sini! Tulis sesuatu secara awam, atau ikuti pengguna daripada pelayan lain secara manual untuk mengisinya",
   "error.unexpected_crash.explanation": "Disebabkan pepijat dalam kod kami atau masalah keserasian pelayar, halaman ini tidak dapat dipaparkan dengan betulnya.",
@@ -291,6 +369,7 @@
   "hashtag.counter_by_uses": "{count, plural, other {{counter} siaran}}",
   "hashtag.counter_by_uses_today": "{count, plural, other {{counter} siaran}} hari ini",
   "hashtag.follow": "Ikuti hashtag",
+  "hashtag.mute": "Redamkan #{hashtag}",
   "hashtag.unfollow": "Nyahikut tanda pagar",
   "hashtags.and_other": "…dan {count, plural, other {# more}}",
   "home.column_settings.show_reblogs": "Tunjukkan galakan",
@@ -300,9 +379,10 @@
   "home.pending_critical_update.link": "Lihat pengemaskinian",
   "home.pending_critical_update.title": "Kemas kini keselamatan kritikal tersedia!",
   "home.show_announcements": "Tunjukkan pengumuman",
+  "interaction_modal.action.favourite": "Untuk sambung, anda perlu suka dari akaun anda.",
   "interaction_modal.on_another_server": "Di pelayan lain",
   "interaction_modal.on_this_server": "Pada pelayan ini",
-  "interaction_modal.title.favourite": "Pos {name} kegemaran",
+  "interaction_modal.title.favourite": "Suka hantaran {name}",
   "interaction_modal.title.follow": "Ikuti {name}",
   "interaction_modal.title.reblog": "Galak hantaran {name}",
   "interaction_modal.title.reply": "Balas siaran {name}",
@@ -310,7 +390,7 @@
   "intervals.full.hours": "{number, plural, other {# jam}}",
   "intervals.full.minutes": "{number, plural, other {# minit}}",
   "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.blocked": "to open blocked users list",
+  "keyboard_shortcuts.blocked": "Buka senarai pengguna tersekat",
   "keyboard_shortcuts.boost": "to boost",
   "keyboard_shortcuts.column": "Tumpu pada lajur",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
@@ -318,8 +398,8 @@
   "keyboard_shortcuts.direct": "to open direct messages column",
   "keyboard_shortcuts.down": "to move down in the list",
   "keyboard_shortcuts.enter": "Buka hantaran",
-  "keyboard_shortcuts.favourite": "Pos kegemaran",
-  "keyboard_shortcuts.favourites": "Buka senarai kegemaran",
+  "keyboard_shortcuts.favourite": "Suka hantaran",
+  "keyboard_shortcuts.favourites": "Buka senarai sukaan",
   "keyboard_shortcuts.federated": "to open federated timeline",
   "keyboard_shortcuts.heading": "Pintasan papan kekunci",
   "keyboard_shortcuts.home": "to open home timeline",
@@ -327,7 +407,7 @@
   "keyboard_shortcuts.legend": "to display this legend",
   "keyboard_shortcuts.local": "to open local timeline",
   "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.muted": "to open muted users list",
+  "keyboard_shortcuts.muted": "Buka senarai pengguna teredam",
   "keyboard_shortcuts.my_profile": "to open your profile",
   "keyboard_shortcuts.notifications": "to open notifications column",
   "keyboard_shortcuts.open_media": "to open media",
@@ -357,24 +437,27 @@
   "load_pending": "{count, plural, one {# item baharu} other {# item baharu}}",
   "loading_indicator.label": "Memuatkan…",
   "moved_to_account_banner.text": "Akaun anda {disabledAccount} kini dinyahdayakan kerana anda berpindah ke {movedToAccount}.",
+  "mute_modal.indefinite": "Sehingga dinyahredamkan",
+  "mute_modal.they_wont_know": "Dia tidak akan tahu bahawa dia telah diredam.",
+  "mute_modal.title": "Redamkan pengguna?",
   "navigation_bar.about": "Perihal",
   "navigation_bar.advanced_interface": "Buka dalam antara muka web lanjutan",
-  "navigation_bar.blocks": "Pengguna yang disekat",
+  "navigation_bar.blocks": "Pengguna tersekat",
   "navigation_bar.bookmarks": "Tanda buku",
   "navigation_bar.community_timeline": "Garis masa tempatan",
   "navigation_bar.compose": "Karang hantaran baharu",
   "navigation_bar.direct": "Sebutan peribadi",
   "navigation_bar.discover": "Teroka",
-  "navigation_bar.domain_blocks": "Domain disekat",
+  "navigation_bar.domain_blocks": "Domain tersekat",
   "navigation_bar.explore": "Teroka",
-  "navigation_bar.favourites": "Kegemaran",
-  "navigation_bar.filters": "Perkataan yang dibisukan",
+  "navigation_bar.favourites": "Sukaan",
+  "navigation_bar.filters": "Perkataan teredam",
   "navigation_bar.follow_requests": "Permintaan ikutan",
   "navigation_bar.followed_tags": "Ikuti hashtag",
   "navigation_bar.follows_and_followers": "Ikutan dan pengikut",
   "navigation_bar.lists": "Senarai",
   "navigation_bar.logout": "Log keluar",
-  "navigation_bar.mutes": "Pengguna yang dibisukan",
+  "navigation_bar.mutes": "Pengguna teredam",
   "navigation_bar.opened_in_classic_interface": "Kiriman, akaun dan halaman tertentu yang lain dibuka secara lalai di antara muka web klasik.",
   "navigation_bar.personal": "Peribadi",
   "navigation_bar.pins": "Hantaran disemat",
@@ -385,11 +468,17 @@
   "not_signed_in_indicator.not_signed_in": "Anda perlu daftar masuk untuk mencapai sumber ini.",
   "notification.admin.report": "{name} melaporkan {target}",
   "notification.admin.sign_up": "{name} mendaftar",
-  "notification.favourite": "{name} menggemari pos anda",
+  "notification.favourite": "{name} menyukai hantaran anda",
+  "notification.favourite.name_and_others_with_link": "{name} dan <a>{count, plural, other {# orang lain}}</a> telah suka hantaran anda",
+  "notification.favourite_pm": "{name} menyukai sebutan persendirian anda",
+  "notification.favourite_pm.name_and_others_with_link": "{name} dan <a>{count, plural, other {# orang lain}}</a> telah suka sebutan persendirian anda",
   "notification.follow": "{name} mengikuti anda",
   "notification.follow_request": "{name} meminta untuk mengikuti anda",
   "notification.own_poll": "Undian anda telah tamat",
   "notification.reblog": "{name} menggalak hantaran anda",
+  "notification.reblog.name_and_others_with_link": "{name} dan <a>{count, plural, other {# orang lain}}</a> telah galakkan hantaran anda",
+  "notification.relationships_severance_event.domain_block": "Pentadbir dari {from} telah menyekat {target} termasuk {followersCount} pengikut anda dan {followingCount, plural, other {# akaun}} ikutan anda.",
+  "notification.relationships_severance_event.user_domain_block": "Anda telah menyekat {target} termasuk {followersCount} pengikut anda dan {followingCount, plural, other {# akaun}} ikutan anda.",
   "notification.status": "{name} baru sahaja mengirim hantaran",
   "notification.update": "{name} menyunting hantaran",
   "notifications.clear": "Buang pemberitahuan",
@@ -397,7 +486,7 @@
   "notifications.column_settings.admin.report": "Laporan baru:",
   "notifications.column_settings.admin.sign_up": "Pendaftaran baru:",
   "notifications.column_settings.alert": "Pemberitahuan atas meja",
-  "notifications.column_settings.favourite": "Kegemaran:",
+  "notifications.column_settings.favourite": "Sukaan:",
   "notifications.column_settings.follow": "Pengikut baharu:",
   "notifications.column_settings.follow_request": "Permintaan ikutan baharu:",
   "notifications.column_settings.mention": "Sebutan:",
@@ -412,7 +501,7 @@
   "notifications.column_settings.update": "Suntingan:",
   "notifications.filter.all": "Semua",
   "notifications.filter.boosts": "Galakan",
-  "notifications.filter.favourites": "Kegemaran",
+  "notifications.filter.favourites": "Sukaan",
   "notifications.filter.follows": "Ikutan",
   "notifications.filter.mentions": "Sebutan",
   "notifications.filter.polls": "Keputusan undian",
@@ -447,7 +536,6 @@
   "poll_button.remove_poll": "Buang undian",
   "privacy.change": "Ubah privasi hantaran",
   "privacy.direct.long": "Semua orang yang disebutkan dalam siaran",
-  "privacy.direct.short": "Orang tertentu",
   "privacy.private.long": "Pengikut anda sahaja",
   "privacy.private.short": "Pengikut",
   "privacy.public.short": "Awam",
@@ -470,7 +558,7 @@
   "reply_indicator.cancel": "Batal",
   "reply_indicator.poll": "Undian",
   "report.block": "Sekat",
-  "report.block_explanation": "Anda tidak akan melihat hantaran mereka. Mereka tidak dapat melihat hantaran anda atau mengikuti anda. Mereka akan sedar bahawa mereka disekat.",
+  "report.block_explanation": "Anda tidak akan melihat hantarannya. Dia tidak akan dapat melihat hantaran anda atau mengikuti anda. Dia akan sedar bahawa dia disekat.",
   "report.categories.legal": "Sah",
   "report.categories.other": "Lain-lain",
   "report.categories.spam": "Spam",
@@ -483,7 +571,7 @@
   "report.comment.title": "Adakah ada hal-hal lain yang perlu kita ketahui?",
   "report.forward": "Panjangkan ke {target}",
   "report.forward_hint": "Akaun ini daripada pelayan lain. Hantar salinan laporan yang ditanpanamakan ke sana juga?",
-  "report.mute": "Bisukan",
+  "report.mute": "Redam",
   "report.mute_explanation": "Anda tidak akan melihat siaran mereka. Mereka masih boleh mengikuti dan melihat siaran anda dan tidak akan mengetahui bahawa mereka telah dibisukan.",
   "report.next": "Seterusnya",
   "report.placeholder": "Ulasan tambahan",
@@ -547,7 +635,7 @@
   "status.admin_status": "Buka hantaran ini dalam antara muka penyederhanaan",
   "status.block": "Sekat @{name}",
   "status.bookmark": "Tanda buku",
-  "status.cancel_reblog_private": "Nyahgalak",
+  "status.cancel_reblog_private": "Nyahgalakkan",
   "status.cannot_reblog": "Hantaran ini tidak boleh digalakkan",
   "status.copy": "Salin pautan ke hantaran",
   "status.delete": "Padam",
@@ -556,7 +644,8 @@
   "status.direct_indicator": "Sebutan peribadi",
   "status.edit": "Sunting",
   "status.edited_x_times": "Disunting {count, plural, other {{count} kali}}",
-  "status.favourite": "Kegemaran",
+  "status.favourite": "Suka",
+  "status.favourites": "{count, plural, other {sukaan}}",
   "status.filter": "Tapiskan hantaran ini",
   "status.history.created": "{name} mencipta pada {date}",
   "status.history.edited": "{name} menyunting pada {date}",
@@ -566,18 +655,20 @@
   "status.media_hidden": "Media disembunyikan",
   "status.mention": "Sebut @{name}",
   "status.more": "Lagi",
-  "status.mute": "Bisukan @{name}",
-  "status.mute_conversation": "Bisukan perbualan",
+  "status.mute": "Redamkan @{name}",
+  "status.mute_conversation": "Redamkan perbualan",
   "status.open": "Kembangkan hantaran ini",
   "status.pin": "Semat di profil",
   "status.pinned": "Hantaran disemat",
   "status.read_more": "Baca lagi",
   "status.reblog": "Galakkan",
-  "status.reblog_private": "Galakkan dengan kebolehlihatan asal",
-  "status.reblogged_by": "{name} telah menggalakkan",
-  "status.reblogs.empty": "Tiada sesiapa yang menggalak hantaran ini. Apabila ada yang menggalak, ia akan muncul di sini.",
+  "status.reblog_private": "Galakkan dengan ketampakan asal",
+  "status.reblogged_by": "{name} galakkan",
+  "status.reblogs": "{count, plural, other {galakan}}",
+  "status.reblogs.empty": "Tiada sesiapa yang galakkan hantaran ini. Apabila ada yang galakkan, hantaran akan muncul di sini.",
   "status.redraft": "Padam & rangka semula",
   "status.remove_bookmark": "Buang tanda buku",
+  "status.remove_favourite": "Padam dari sukaan",
   "status.replied_to": "Menjawab kepada {name}",
   "status.reply": "Balas",
   "status.replyAll": "Balas ke bebenang",
@@ -591,7 +682,7 @@
   "status.translate": "Menterjemah",
   "status.translated_from_with": "Diterjemah daripada {lang} dengan {provider}",
   "status.uncached_media_warning": "Pratonton tidak tersedia",
-  "status.unmute_conversation": "Nyahbisukan perbualan",
+  "status.unmute_conversation": "Nyahredamkan perbualan",
   "status.unpin": "Nyahsemat daripada profil",
   "subscribed_languages.lead": "Hanya hantaran dalam bahasa-bahasa terpilih akan dipaparkan pada garis masa rumah dan senarai selepas perubahan. Pilih tiada untuk menerima hantaran dalam semua bahasa.",
   "subscribed_languages.save": "Simpan perubahan",
@@ -613,6 +704,8 @@
   "upload_button.label": "Tambah fail imej, video atau audio",
   "upload_error.limit": "Sudah melebihi had muat naik.",
   "upload_error.poll": "Tidak boleh memuat naik fail bersama undian.",
+  "upload_form.drag_and_drop.instructions": "Untuk mengangkat lampiran media, tekan jarak atau enter. Ketika menarik, gunakan kekunci anak panah untuk menggerakkan lampiran media pada mana-mana arah. Tekan jarak atau enter untuk melepaskan lampiran media pada kedudukan baharunya, atau tekan keluar untuk batalkan.",
+  "upload_form.drag_and_drop.on_drag_cancel": "Seretan dibatalkan. Lampiran media {item} dilepaskan.",
   "upload_form.edit": "Sunting",
   "upload_progress.label": "Memuat naik...",
   "upload_progress.processing": "Memproses…",
@@ -623,8 +716,8 @@
   "video.expand": "Besarkan video",
   "video.fullscreen": "Skrin penuh",
   "video.hide": "Sembunyikan video",
-  "video.mute": "Bisukan bunyi",
+  "video.mute": "Redam",
   "video.pause": "Jeda",
   "video.play": "Main",
-  "video.unmute": "Nyahbisukan bunyi"
+  "video.unmute": "Nyahredam"
 }
diff --git a/app/javascript/mastodon/locales/my.json b/app/javascript/mastodon/locales/my.json
index 3d62c4329a..362537edeb 100644
--- a/app/javascript/mastodon/locales/my.json
+++ b/app/javascript/mastodon/locales/my.json
@@ -28,7 +28,6 @@
   "account.endorse": "အကောင့်ပရိုဖိုင်တွင်ဖော်ပြပါ",
   "account.featured_tags.last_status_at": "နောက်ဆုံးပို့စ်ကို {date} တွင် တင်ခဲ့သည်။",
   "account.featured_tags.last_status_never": "ပို့စ်တင်ထားခြင်းမရှိပါ",
-  "account.featured_tags.title": "ဖော်ပြထားသောဟက်ရှ်တက်ခ်များ",
   "account.follow": "စောင့်ကြည့်",
   "account.followers": "စောင့်ကြည့်သူများ",
   "account.followers.empty": "ဤသူကို စောင့်ကြည့်သူ မရှိသေးပါ။",
@@ -598,8 +597,6 @@
   "video.expand": "ဗီဒီယိုကို ချဲ့ပါ",
   "video.fullscreen": "မျက်နှာပြင်အပြည့်",
   "video.hide": "ဗီဒီယိုကို ဖျောက်ပါ",
-  "video.mute": "အသံပိတ်ထားပါ",
   "video.pause": "ခဏရပ်ပါ",
-  "video.play": "ဖွင့်ပါ",
-  "video.unmute": "အသံပြန်ဖွင့်ပါ"
+  "video.play": "ဖွင့်ပါ"
 }
diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json
index 110d5a7301..2c5af1d406 100644
--- a/app/javascript/mastodon/locales/nan.json
+++ b/app/javascript/mastodon/locales/nan.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "編輯個人資料",
   "account.enable_notifications": "佇 {name} PO文ê時通知我",
   "account.endorse": "用個人資料推薦對方",
+  "account.featured": "精選ê",
+  "account.featured.hashtags": "Hashtag",
+  "account.featured.posts": "PO文",
   "account.featured_tags.last_status_at": "頂kái tī {date} Po文",
   "account.featured_tags.last_status_never": "無PO文",
-  "account.featured_tags.title": "{name} ê推薦hashtag",
   "account.follow": "跟tuè",
   "account.follow_back": "Tuè tńg去",
   "account.followers": "跟tuè lí ê",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, other {{count} ê PO文}}",
   "account.unblock": "取消封鎖 @{name}",
   "account.unblock_domain": "Kā域名 {domain} 取消封鎖",
+  "account.unblock_domain_short": "取消封鎖",
   "account.unblock_short": "取消封鎖",
   "account.unendorse": "Mài tī個人資料推薦伊",
   "account.unfollow": "取消跟tuè",
@@ -86,6 +89,13 @@
   "alert.unexpected.message": "發生意外ê錯誤。.",
   "alert.unexpected.title": "Ai-ioh!",
   "alt_text_badge.title": "替代文字",
+  "alt_text_modal.add_alt_text": "加添說明文字",
+  "alt_text_modal.add_text_from_image": "Tuì圖加說明文字",
+  "alt_text_modal.cancel": "取消",
+  "alt_text_modal.change_thumbnail": "改縮小圖",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "請替聽有困難ê敘述tsit ê內容…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "請替看有困難ê敘述tsit ê內容…",
+  "alt_text_modal.done": "做好ah",
   "announcement.announcement": "公告",
   "annual_report.summary.archetype.booster": "追求趣味ê",
   "annual_report.summary.archetype.lurker": "有讀無PO ê",
@@ -182,9 +192,402 @@
   "compose_form.poll.multiple": "Tsē選擇",
   "compose_form.poll.option_placeholder": "選項 {number}",
   "compose_form.poll.single": "單選擇",
+  "compose_form.poll.switch_to_multiple": "Kā投票改做ē當選tsē-tsē ê。",
+  "compose_form.poll.switch_to_single": "Kā投票改做kan-ta通選tsi̍t-ê",
+  "compose_form.poll.type": "投票ê方法",
+  "compose_form.publish": "PO文",
+  "compose_form.publish_form": "PO出去",
+  "compose_form.reply": "回應",
+  "compose_form.save_changes": "更新",
+  "compose_form.spoiler.marked": "Thâi掉內容警告",
+  "compose_form.spoiler.unmarked": "加添內容警告",
+  "compose_form.spoiler_placeholder": "內容警告(m̄是必要)",
+  "confirmation_modal.cancel": "取消",
+  "confirmations.block.confirm": "封鎖",
+  "confirmations.delete.confirm": "Thâi掉",
+  "confirmations.delete.message": "Lí kám確定beh thâi掉tsit ê PO文?",
+  "confirmations.delete.title": "Kám beh thâi掉tsit ê PO文?",
+  "confirmations.delete_list.confirm": "Thâi掉",
+  "confirmations.delete_list.message": "Lí kám確定beh永永thâi掉tsit ê列單?",
+  "confirmations.delete_list.title": "Kám beh thâi掉tsit ê列單?",
+  "confirmations.discard_edit_media.confirm": "棄sak",
+  "confirmations.discard_edit_media.message": "Lí佇媒體敘述á是先看māi ê所在有iáu buē儲存ê改變,kám beh kā in棄sak?",
+  "confirmations.edit.confirm": "編輯",
+  "confirmations.edit.message": "Tsit-má編輯ē khàm掉lí tng-leh編寫ê訊息,lí kám beh繼續án-ne做?",
+  "confirmations.edit.title": "Kám beh khàm掉PO文?",
   "confirmations.follow_to_list.confirm": "跟tuè,加入kàu列單",
+  "confirmations.follow_to_list.message": "Beh kā {name} 加添kàu列單,lí tio̍h先跟tuè伊。",
+  "confirmations.follow_to_list.title": "Kám beh跟tuè tsit ê用者?",
+  "confirmations.logout.confirm": "登出",
+  "confirmations.logout.message": "Lí kám確定beh登出?",
+  "confirmations.logout.title": "Lí kám beh登出?",
+  "confirmations.missing_alt_text.confirm": "加添說明文字",
+  "confirmations.missing_alt_text.message": "Lí ê PO文包含無說明文字ê媒體。加添敘述,ē幫tsān lí ê內容hōo khah tsē lâng接近使用。",
+  "confirmations.missing_alt_text.secondary": "就按呢PO出去",
+  "confirmations.missing_alt_text.title": "Kám beh加添說明文字?",
+  "confirmations.mute.confirm": "消音",
+  "confirmations.redraft.confirm": "Thâi掉了後重寫",
+  "confirmations.redraft.message": "Lí kám確定behthâi掉tsit篇PO文了後koh重寫?收藏kap轉PO ē無去,而且原底ê PO文ê回應ē變孤立。",
+  "confirmations.redraft.title": "Kám beh thâi掉koh重寫PO文?",
+  "confirmations.reply.confirm": "回應",
+  "confirmations.reply.message": "Tsit-má回應ē khàm掉lí tng-leh編寫ê訊息。Lí kám確定beh繼續án-ne做?",
+  "confirmations.reply.title": "Kám beh khàm掉PO文?",
+  "confirmations.unfollow.confirm": "取消跟tuè",
+  "confirmations.unfollow.message": "Lí kám確定無愛跟tuè {name}?",
+  "confirmations.unfollow.title": "Kám beh取消跟tuè tsit ê用者?",
+  "content_warning.hide": "Am-khàm PO文",
+  "content_warning.show": "Mā tio̍h顯示",
+  "content_warning.show_more": "其他內容",
+  "conversation.delete": "Thâi掉會話",
+  "conversation.mark_as_read": "標做有讀",
+  "conversation.open": "顯示會話",
+  "conversation.with": "Kap {names}",
+  "copy_icon_button.copied": "有khóo-pih kàu tsián貼pang",
+  "copypaste.copied": "有khóo-pih",
+  "copypaste.copy_to_clipboard": "Khóo-pih kàu tsián貼pang",
+  "directory.federated": "Uì知影ê Fediverse",
+  "directory.local": "Kan-ta uì {domain}",
+  "directory.new_arrivals": "新來ê",
+  "directory.recently_active": "最近活動ê",
+  "disabled_account_banner.account_settings": "口座ê設定",
+  "disabled_account_banner.text": "Lí ê口座 {disabledAccount} tsit-má hōo lâng停止使用。",
+  "dismissable_banner.community_timeline": "Tsia sī uì 口座hē tī {domain} ê lâng,最近所公開PO ê。",
+  "dismissable_banner.dismiss": "Mài kā tshah",
+  "dismissable_banner.explore_links": "Tsiah ê新聞故事是kín-á日tī Fediverse hōo lâng分享上tsē pái ê。Hōo koh khah tsē lâng 分享ê khah新ê新聞故事,名次koh khah kuân。",
+  "dismissable_banner.explore_statuses": "Tsiah ê tuì Fediverse來ê PO文kín-á日有真tsē關注。Khah新ê,有khah tsē轉送kap收藏ê,名次koh khah kuân。",
+  "dismissable_banner.explore_tags": "Tsiah ê hashtag kín-á日tī Fediverse 有真tsē關注。Khah tsē無kâng ê lâng leh用ê hashtag,名次koh khah kuân。",
+  "dismissable_banner.public_timeline": "Tsiah ê是 {domain} 內底ê lâng 佇 Fediverse所跟tuè ê ê,上新ê公開PO文。.",
+  "domain_block_modal.block": "封鎖服侍器",
+  "domain_block_modal.block_account_instead": "改做封鎖 @{name}",
+  "domain_block_modal.they_can_interact_with_old_posts": "Uì tsit ê服侍器來ê,通kap lí khah早ê PO交流。",
+  "domain_block_modal.they_cant_follow": "Tuì tsit ê服侍器來ê 通跟tuè lí。",
+  "domain_block_modal.they_wont_know": "In buē知影in受封鎖。",
+  "domain_block_modal.title": "Kám beh封鎖域名?",
+  "domain_block_modal.you_will_lose_num_followers": "Lí ē失去 {followersCount, plural, other {{followersCountDisplay} ê lâng跟tuè}} kap {followingCount, plural, other {{followingCountDisplay} ê lí所tuè ê 口座}}。",
+  "domain_block_modal.you_will_lose_relationships": "Lí ē失去逐ê佇tsit ê服侍器跟tuè lí ê,kap lí所跟tuè ê。",
+  "domain_block_modal.you_wont_see_posts": "Lí buē看見tsit ê服侍器ê用者所送ê PO文kap通知。",
+  "domain_pill.activitypub_lets_connect": "伊ē hōo lí kap Mastodon ê lâng連結kap互動,其他社交應用程式ê lâng mā ē使。",
+  "domain_pill.activitypub_like_language": "ActivityPub親像Mastodon kap其他社交應用程式所講ê語言。",
+  "domain_pill.server": "服侍器",
+  "domain_pill.their_handle": "In ê口座:",
+  "domain_pill.their_server": "In數位ê tau,in所有ê PO文lóng tī tsia。",
+  "domain_pill.their_username": "In佇in ê服侍器獨一ê稱呼。佇無kâng ê服侍器有可能tshuē著kāng名ê用者。",
+  "domain_pill.username": "用者ê名",
+  "domain_pill.whats_in_a_handle": "口座是siánn-mih?",
+  "domain_pill.who_they_are": "因為口座(handle)表示tsit ê lâng是siáng kap tī toh,lí ē當佇<button>支援ActivityPub ê平臺</button>. ê社交網路kap lâng交流。",
+  "domain_pill.who_you_are": "因為口座(handle)表示lí是siáng kap tī toh,lâng ē當佇<button>支援ActivityPub ê平臺</button>. ê社交網路kap lí交流。",
+  "domain_pill.your_handle": "Lí ê口座:",
+  "domain_pill.your_server": "Lí數位ê厝,內底有lí所有ê PO文。無kah意?Ē當轉kàu別ê服侍器,koh保有跟tuè lí êl âng。.",
+  "domain_pill.your_username": "Lí 佇tsit ê服侍器獨一ê稱呼。佇無kâng ê服侍器有可能tshuē著kāng名ê用者。",
+  "embed.instructions": "Khóo-pih 下kha ê程式碼,來kā tsit篇PO文tàu佇lí ê網站。",
+  "embed.preview": "伊e án-ne顯示:\n",
+  "emoji_button.activity": "活動",
+  "emoji_button.clear": "清掉",
+  "emoji_button.custom": "自訂ê",
+  "emoji_button.flags": "旗á",
+  "emoji_button.food": "Tsia̍h-mi̍h kap 飲料",
+  "emoji_button.label": "加入繪文字(emoji)",
+  "emoji_button.nature": "自然",
+  "emoji_button.not_found": "Tshuē無對應ê emoji",
+  "emoji_button.objects": "物件",
+  "emoji_button.people": "Lâng",
+  "emoji_button.recent": "Tsia̍p用ê",
+  "emoji_button.search": "Tshiau-tshuē……",
+  "emoji_button.search_results": "Tshiau-tshuē ê結果",
+  "emoji_button.symbols": "符號",
+  "emoji_button.travel": "旅行kap地點",
+  "empty_column.account_featured": "Tsit ê列單是空ê",
+  "empty_column.account_hides_collections": "Tsit位用者選擇無愛公開tsit ê資訊",
+  "empty_column.account_suspended": "口座已經受停止",
+  "empty_column.account_timeline": "Tsia無PO文!",
+  "empty_column.account_unavailable": "個人資料bē當看",
+  "empty_column.blocks": "Lí iáu無封鎖任何用者。",
+  "empty_column.bookmarked_statuses": "Lí iáu無加添任何冊籤。Nā是lí加添冊籤,伊ē佇tsia顯示。",
+  "empty_column.community": "本站時間線是空ê。緊來公開PO文oh!",
+  "empty_column.direct": "Lí iáu無任何ê私人訊息。Nā是lí送á是收著私人訊息,ē佇tsia顯示。.",
+  "empty_column.domain_blocks": "Iáu無封鎖任何網域。",
+  "empty_column.explore_statuses": "目前iáu無有流行ê趨勢,請sió等tsi̍t-ē,koh確認。",
+  "empty_column.favourited_statuses": "Lí iáu無加添任何收藏 ê PO文。Nā是lí加收藏,伊ē佇tsia顯示。",
+  "empty_column.favourites": "Iáu無lâng收藏tsit篇PO文。Nā是有lâng收藏,ē佇tsia顯示。",
+  "empty_column.follow_requests": "Lí iáu buē收著任何ê跟tuè請求。Nā是lí收著,伊ē佇tsia顯示。",
+  "empty_column.followed_tags": "Lí iáu buē收著任何ê hashtag。Nā是lí收著,ē佇tsia顯示。",
+  "empty_column.hashtag": "Tsit ê hashtag內底無物件。",
+  "empty_column.home": "Lí tshù ê時間線是空ê!跟tuè別lâng來kā充滿。",
+  "empty_column.list": "Tsit張列單內底iáu bô物件。若是列單內底ê成員貼新ê PO文,in ē tī tsia顯示。",
+  "empty_column.mutes": "Lí iáu無消音任何用者。",
+  "empty_column.notification_requests": "清hōo空ah!內底無物件。若是lí收著新ê通知,ē根據lí ê設定,佇tsia出現。",
+  "empty_column.notifications": "Lí iáu無收著任何通知。Nā別lâng kap lí互動,lí ē佇tsia看著。",
+  "empty_column.public": "內底無物件!寫beh公開ê PO文,á是主動跟tuè別ê服侍器ê用者,來加添內容。",
+  "error.unexpected_crash.explanation": "因為原始碼內底有錯誤,á是瀏覽器相容出tshê,tsit頁bē當正確顯示。",
+  "error.unexpected_crash.explanation_addons": "Tsit頁bē當正確顯示,可能是瀏覽器附ê功能,á是自動翻譯工具所致。",
+  "error.unexpected_crash.next_steps": "請試更新tsit頁。若是bē當改善,lí iáu是ē當改使用無kâng ê瀏覽器,á是app,來用Mastodon。",
+  "error.unexpected_crash.next_steps_addons": "請試kā in停止使用,suà落來更新tsit頁。若是bē當改善,lí iáu是ē當改使用無kâng ê瀏覽器,á是app,來用Mastodon。",
+  "errors.unexpected_crash.copy_stacktrace": "Khóo-pih stacktrace kàu剪貼pang-á",
+  "errors.unexpected_crash.report_issue": "報告問題",
+  "explore.suggested_follows": "用者",
+  "explore.title": "探索",
+  "explore.trending_links": "新聞",
+  "explore.trending_statuses": "PO文",
+  "explore.trending_tags": "Hashtag",
+  "filter_modal.added.context_mismatch_explanation": "Tsit ê過濾器類別bē當適用佇lí所接近使用ê PO文ê情境。若是lí mā beh佇tsit ê情境過濾tsit ê PO文,lí著編輯過濾器。.",
+  "filter_modal.added.context_mismatch_title": "本文無sio合!",
+  "filter_modal.added.expired_explanation": "Tsit ê過濾器類別過期ah,lí需要改到期ê日期來繼續用。",
+  "filter_modal.added.expired_title": "過期ê過濾器",
+  "filter_modal.added.review_and_configure": "Beh審視kap進前設定tsit ê過濾器ê類別,請kàu {settings_link}。",
+  "filter_modal.added.review_and_configure_title": "過濾器ê設定",
+  "filter_modal.added.settings_link": "設定頁",
+  "filter_modal.added.short_explanation": "Tsit ê PO文已經加添kàu下kha ê過濾器類別:{title}。",
+  "filter_modal.added.title": "過濾器加添ah!",
+  "filter_modal.select_filter.context_mismatch": "Mài用tī tsit ê內文",
+  "filter_modal.select_filter.expired": "過期ah",
+  "filter_modal.select_filter.prompt_new": "新ê類別:{name}",
+  "filter_modal.select_filter.search": "Tshiau-tshuē á是加添",
+  "filter_modal.select_filter.subtitle": "用有ê類別á是建立新ê",
+  "filter_modal.select_filter.title": "過濾tsit ê PO文",
+  "filter_modal.title.status": "過濾PO文",
+  "filter_warning.matches_filter": "合過濾器「<span>{title}</span>」",
+  "filtered_notifications_banner.pending_requests": "Tuì lí可能熟sāi ê {count, plural, =0 {0 ê人} other {# ê人}}",
+  "filtered_notifications_banner.title": "過濾ê通知",
+  "firehose.all": "Kui ê",
+  "firehose.local": "Tsit ê服侍器",
+  "firehose.remote": "別ê服侍器",
+  "follow_request.authorize": "授權",
+  "follow_request.reject": "拒絕",
+  "follow_requests.unlocked_explanation": "就算lí ê口座無hőng鎖,{domain} ê管理員leh想,lí可能beh手動審查tuì tsiah ê口座送ê跟tuè請求。",
+  "follow_suggestions.curated_suggestion": "精選ê內容",
+  "follow_suggestions.dismiss": "Mài koh顯示。",
+  "follow_suggestions.featured_longer": "{domain} 團隊所揀ê",
+  "follow_suggestions.friends_of_friends_longer": "時行佇lí所tuè ê lâng",
+  "follow_suggestions.hints.featured": "Tsit ê個人資料是 {domain} 團隊特別揀ê。",
+  "follow_suggestions.hints.friends_of_friends": "Tsit ê個人資料tī lí跟tuè ê lâng之間真流行。",
+  "follow_suggestions.hints.most_followed": "Tsit ê個人資料是 {domain} 內,有足tsē跟tuè者ê其中tsit ê。",
+  "follow_suggestions.hints.most_interactions": "Tsit ê個人資料tsi̍t-tsām-á佇 {domain} 有得著真tsē關注。",
+  "follow_suggestions.hints.similar_to_recently_followed": "Tsit ê個人資料kap lí最近跟tuè ê口座相siâng。",
+  "follow_suggestions.personalized_suggestion": "個人化ê推薦",
+  "follow_suggestions.popular_suggestion": "流行ê推薦",
+  "follow_suggestions.popular_suggestion_longer": "佇{domain} 足有lâng緣",
+  "follow_suggestions.similar_to_recently_followed_longer": "Kap lí最近跟tuè ê相siâng",
+  "follow_suggestions.view_all": "看全部",
+  "follow_suggestions.who_to_follow": "Thang tuè ê",
+  "followed_tags": "跟tuè ê hashtag",
+  "footer.about": "概要",
+  "footer.directory": "個人資料ê目錄",
+  "footer.get_app": "The̍h著app",
+  "footer.keyboard_shortcuts": "鍵盤kiu-té khí (shortcut)",
+  "footer.privacy_policy": "隱私權政策",
+  "footer.source_code": "看原始碼",
+  "footer.status": "狀態",
+  "footer.terms_of_service": "服務規定",
+  "generic.saved": "儲存ah",
+  "getting_started.heading": "開始用",
+  "hashtag.admin_moderation": "Phah開 #{name} ê管理界面",
+  "hashtag.browse": "瀏覽佇 #{hashtag} ê PO文",
+  "hashtag.browse_from_account": "瀏覽 @{name} 佇 #{hashtag} 所寫ê PO文",
+  "hashtag.column_header.tag_mode.all": "kap {additional}",
+  "hashtag.column_header.tag_mode.any": "á是 {additional}",
+  "hashtag.column_header.tag_mode.none": "無需要 {additional}",
+  "hashtag.column_settings.select.no_options_message": "Tshuē無建議",
+  "hashtag.column_settings.select.placeholder": "請輸入hashtag……",
+  "hashtag.column_settings.tag_mode.all": "Kui ê",
+  "hashtag.column_settings.tag_mode.any": "任何tsi̍t ê",
+  "hashtag.column_settings.tag_mode.none": "Lóng mài",
+  "hashtag.column_settings.tag_toggle": "Kā追加ê標籤加添kàu tsit ê欄",
+  "hashtag.counter_by_accounts": "{count, plural, one {{counter} ê} other {{counter} ê}}參與ê",
+  "hashtag.counter_by_uses": "{count, plural, one {{counter} 篇} other {{counter} 篇}} PO文",
+  "hashtag.counter_by_uses_today": "Kin-á日有 {count, plural, one {{counter} 篇} other {{counter} 篇}} PO文",
+  "hashtag.follow": "跟tuè hashtag",
+  "hashtag.mute": "消音 #{hashtag}",
+  "hashtag.unfollow": "取消跟tuè hashtag",
+  "hashtags.and_other": "……kap 其他 {count, plural, other {# ê}}",
+  "hints.profiles.followers_may_be_missing": "Tsit ê個人資料ê跟tuè者資訊可能有落勾ê。",
+  "hints.profiles.follows_may_be_missing": "Tsit ê口座所跟tuè ê ê資訊可能有落勾ê。",
+  "hints.profiles.posts_may_be_missing": "Tsit ê口座ê tsi̍t kuá PO文可能有落勾ê。",
+  "hints.profiles.see_more_followers": "佇 {domain} 看koh khah tsē跟tuè lí ê",
+  "hints.profiles.see_more_follows": "佇 {domain} 看koh khah tsē lí跟tuè ê",
+  "hints.profiles.see_more_posts": "佇 {domain} 看koh khah tsē ê PO文",
+  "hints.threads.replies_may_be_missing": "Tuì其他ê服侍器來ê回應可能有phah m̄見。",
+  "hints.threads.see_more": "佇 {domain} 看koh khah tsē ê回應",
+  "home.column_settings.show_reblogs": "顯示轉PO",
+  "home.column_settings.show_replies": "顯示回應",
+  "home.hide_announcements": "Khàm掉公告",
+  "home.pending_critical_update.body": "請liōng早更新lí ê Mastodon ê服侍器!",
+  "home.pending_critical_update.link": "看更新內容",
+  "home.pending_critical_update.title": "有重要ê安全更新!",
+  "home.show_announcements": "顯示公告",
+  "ignore_notifications_modal.disclaimer": "Lí所忽略in ê通知ê用者,Mastodonbē當kā lí通知。忽略通知bē當阻擋訊息ê寄送。",
+  "ignore_notifications_modal.filter_instead": "改做過濾",
+  "ignore_notifications_modal.filter_to_act_users": "Lí猶原ē當接受、拒絕猶是檢舉用者",
+  "ignore_notifications_modal.filter_to_avoid_confusion": "過濾ē當避免可能ê bē分明。",
+  "ignore_notifications_modal.filter_to_review_separately": "Lí ē當個別檢視所過濾ê通知",
+  "ignore_notifications_modal.ignore": "Kā通知忽略",
+  "ignore_notifications_modal.limited_accounts_title": "Kám beh忽略受限制ê口座送來ê通知?",
+  "ignore_notifications_modal.new_accounts_title": "Kám beh忽略新口座送來ê通知?",
+  "ignore_notifications_modal.not_followers_title": "Kám beh忽略無跟tuè lí ê口座送來ê通知?",
+  "ignore_notifications_modal.not_following_title": "Kám beh忽略lí 無跟tuè ê口座送來ê通知?",
+  "ignore_notifications_modal.private_mentions_title": "忽略ka-kī主動送ê私人提起ê通知?",
+  "info_button.label": "幫tsān",
+  "info_button.what_is_alt_text": "<h1>Siánn物是替代文字?</h1> <p>替代文字kā視覺有障礙、網路速度khah慢,á是beh tshuē頂下文ê lâng,提供圖ê敘述。</p> <p>Lí ē當通過寫明白、簡單kap客觀ê替代文字,替逐家改善容易使用性kap幫tsān理解。</p> <ul> <li>掌握重要ê因素</li> <li>替圖寫摘要ê文字</li> <li>用規則ê語句結構</li> <li>避免重複ê資訊</li> <li>專注佇趨勢kap佇複雜視覺(比如圖表á是地圖)內底tshuē關鍵</li> </ul>",
+  "interaction_modal.action.favourite": "Nā beh繼續,lí tio̍h用你ê口座收藏。",
+  "interaction_modal.action.follow": "Nā beh繼續,lí tio̍h用你ê口座跟tuè。",
+  "interaction_modal.action.reblog": "Nā beh繼續,lí tio̍h用你ê口座轉送。",
+  "interaction_modal.action.reply": "Nā beh繼續,lí tio̍h用你ê口座回應。",
+  "interaction_modal.action.vote": "Nā beh繼續,lí tio̍h用你ê口座投票。",
+  "interaction_modal.go": "行",
+  "interaction_modal.no_account_yet": "Tsit-má iáu bô口座?",
+  "interaction_modal.on_another_server": "佇無kâng ê服侍器",
+  "interaction_modal.on_this_server": "Tī tsit ê服侍器",
+  "interaction_modal.title.favourite": "收藏 {name} ê PO文",
+  "interaction_modal.title.follow": "跟tuè {name}",
+  "interaction_modal.title.reblog": "轉送 {name} ê PO文",
+  "interaction_modal.title.reply": "回應 {name} ê PO文",
+  "interaction_modal.title.vote": "參加 {name} ê投票",
+  "interaction_modal.username_prompt": "比如:{example}",
+  "intervals.full.days": "{number, plural, other {# kang}}",
+  "intervals.full.hours": "{number, plural, other {# 點鐘}}",
+  "intervals.full.minutes": "{number, plural, other {# 分鐘}}",
+  "keyboard_shortcuts.back": "Tńg去",
+  "keyboard_shortcuts.blocked": "開封鎖ê用者ê列單",
+  "keyboard_shortcuts.boost": "轉送PO文",
+  "keyboard_shortcuts.column": "揀tsit ê欄",
+  "keyboard_shortcuts.compose": "揀寫文字ê框仔",
+  "keyboard_shortcuts.description": "說明",
+  "keyboard_shortcuts.direct": "phah開私人提起ê欄",
+  "keyboard_shortcuts.down": "佇列單內kā suá khah 下kha",
+  "keyboard_shortcuts.enter": "Phah開PO文",
+  "keyboard_shortcuts.favourite": "收藏PO文",
+  "keyboard_shortcuts.favourites": "Phah開收藏ê列單",
+  "keyboard_shortcuts.federated": "Phah開聯邦ê時間線",
+  "keyboard_shortcuts.heading": "鍵盤ê快速key",
+  "keyboard_shortcuts.home": "Phah開tshù ê時間線",
+  "keyboard_shortcuts.hotkey": "快速key",
+  "keyboard_shortcuts.legend": "顯示tsit篇說明",
+  "keyboard_shortcuts.local": "Phah開本站ê時間線",
+  "keyboard_shortcuts.mention": "提起作者",
+  "keyboard_shortcuts.muted": "Phah開消音ê用者列單",
+  "keyboard_shortcuts.my_profile": "Phah開lí ê個人資料",
+  "keyboard_shortcuts.notifications": "Phah開通知欄",
+  "keyboard_shortcuts.open_media": "Phah開媒體",
+  "keyboard_shortcuts.pinned": "Phah開釘起來ê PO文列單",
+  "keyboard_shortcuts.profile": "Phah開作者ê個人資料",
+  "keyboard_shortcuts.reply": "回應PO文",
+  "keyboard_shortcuts.requests": "Phah開跟tuè請求ê列單",
+  "keyboard_shortcuts.search": "揀tshiau-tshuē條á",
+  "keyboard_shortcuts.spoilers": "顯示/隱藏內容警告",
+  "keyboard_shortcuts.start": "Phah開「開始用」欄",
+  "keyboard_shortcuts.toggle_hidden": "顯示/隱藏內容警告後壁ê PO文",
+  "keyboard_shortcuts.toggle_sensitivity": "顯示/tshàng媒體",
+  "keyboard_shortcuts.toot": "PO新PO文",
+  "keyboard_shortcuts.translate": "kā PO文翻譯",
+  "keyboard_shortcuts.unfocus": "離開輸入框仔/tshiau-tshuē格仔",
+  "keyboard_shortcuts.up": "佇列單內kā suá khah面頂",
+  "lightbox.close": "關",
+  "lightbox.next": "下tsi̍t ê",
+  "lightbox.previous": "頂tsi̍t ê",
+  "lightbox.zoom_in": "Tshūn-kiu kàu實際ê sài-suh",
+  "lightbox.zoom_out": "Tshūn-kiu kàu適當ê sài-suh",
+  "limited_account_hint.action": "一直顯示個人資料",
+  "limited_account_hint.title": "Tsit ê 個人資料予 {domain} ê管理員tshàng起來ah。",
+  "link_preview.author": "Tuì {name}",
+  "link_preview.more_from_author": "看 {name} ê其他內容",
+  "link_preview.shares": "{count, plural, one {{counter} 篇} other {{counter} 篇}} PO文",
+  "lists.add_member": "加",
+  "lists.add_to_list": "加添kàu列單",
+  "lists.add_to_lists": "Kā {name} 加添kàu列單",
+  "lists.create": "建立",
+  "lists.create_a_list_to_organize": "開新ê列單,組織lí tshù ê時間線",
+  "lists.create_list": "建立列單",
+  "lists.delete": "Thâi掉列單",
+  "lists.done": "做好ah",
+  "lists.edit": "編輯列單",
+  "lists.exclusive": "佇tshù ê時間線kā成員tshàng起來。",
+  "lists.exclusive_hint": "Nā bóo-mi̍h口座佇tsit ê列單,ē tuì lí tshù ê時間線kā tsit ê口座tshàng起來,避免koh看見in ê PO文。",
+  "lists.find_users_to_add": "Tshuē beh加添ê用者",
+  "lists.list_members": "列單ê成員",
+  "lists.list_members_count": "{count, plural, other {# 位成員}}",
+  "lists.list_name": "列單ê名",
+  "lists.new_list_name": "新ê列單ê名",
+  "lists.no_lists_yet": "Iáu無列單。",
+  "lists.no_members_yet": "Iáu無成員。",
+  "lists.no_results_found": "Tshuē無結果。",
+  "lists.remove_member": "Suá掉",
+  "lists.replies_policy.followed": "所跟tuè ê任何用者",
+  "lists.replies_policy.list": "列單ê成員",
+  "lists.replies_policy.none": "無半位",
+  "lists.save": "儲存",
+  "lists.search": "Tshiau-tshuē",
+  "lists.show_replies_to": "列單成員回應ê顯示範圍",
+  "load_pending": "{count, plural, other {# ê 項目}}",
+  "loading_indicator.label": "Leh載入……",
+  "media_gallery.hide": "Khàm掉",
+  "moved_to_account_banner.text": "Lí ê口座 {disabledAccount} 已經停止使用ah,因為suá kàu {movedToAccount}。",
+  "mute_modal.hide_from_notifications": "Tuì通知內底khàm掉",
+  "mute_modal.hide_options": "Khàm掉選項",
+  "mute_modal.indefinite": "直到我取消消音",
+  "mute_modal.show_options": "顯示選項",
+  "mute_modal.they_can_mention_and_follow": "In iáu ē當提起á是跟tuè lí,毋過lí看buē著in。",
+  "mute_modal.they_wont_know": "In buē知影in受消音。",
+  "mute_modal.title": "Kā用者消音?",
+  "mute_modal.you_wont_see_mentions": "Lí buē看見提起in ê PO文。",
+  "mute_modal.you_wont_see_posts": "In iáu ē當看著lí ê PO文,毋過lí看bē tio̍h in ê。",
+  "navigation_bar.about": "概要",
+  "navigation_bar.administration": "管理",
+  "navigation_bar.advanced_interface": "用進階ê網頁界面開",
+  "navigation_bar.blocks": "封鎖ê用者",
+  "navigation_bar.bookmarks": "冊籤",
+  "navigation_bar.community_timeline": "本地ê時間線",
+  "navigation_bar.compose": "寫新ê PO文",
+  "navigation_bar.direct": "私人ê提起",
+  "navigation_bar.discover": "發現",
+  "navigation_bar.domain_blocks": "封鎖ê域名",
+  "navigation_bar.explore": "探查",
+  "navigation_bar.favourites": "Siōng kah意",
+  "navigation_bar.filters": "消音ê詞",
+  "navigation_bar.follow_requests": "跟tuè請求",
+  "navigation_bar.followed_tags": "跟tuè ê hashtag",
+  "navigation_bar.follows_and_followers": "Leh跟tuè ê kap跟tuè lí ê",
+  "navigation_bar.lists": "列單",
+  "navigation_bar.logout": "登出",
+  "navigation_bar.moderation": "審核",
+  "navigation_bar.mutes": "消音ê用者",
+  "navigation_bar.opened_in_classic_interface": "PO文、口座kap其他指定ê頁面,預設ē佇經典ê網頁界面內phah開。",
+  "navigation_bar.personal": "個人",
+  "navigation_bar.pins": "釘起來ê PO文",
+  "navigation_bar.preferences": "偏愛ê設定",
+  "navigation_bar.public_timeline": "聯邦ê時間線",
+  "navigation_bar.search": "Tshiau-tshuē",
+  "navigation_bar.security": "安全",
+  "not_signed_in_indicator.not_signed_in": "Lí著登入來接近使用tsit ê資源。",
+  "notification.admin.report": "{name} kā {target} 檢舉ah",
+  "notification.admin.report_account": "{name} kā {target} 所寫ê {count, plural, other {# 篇PO文}}檢舉ah,原因是:{category}",
+  "notification.admin.report_account_other": "{name} kā {target} 所寫ê {count, plural, other {# 篇PO文}}檢舉ah",
+  "notification.admin.report_statuses": "{name} kā {target} 檢舉ah,原因是:{category}",
+  "notification.admin.report_statuses_other": "{name} kā {target} 檢舉ah",
+  "notification.admin.sign_up": "口座 {name} 有開ah。",
+  "notification.admin.sign_up.name_and_others": "{name} kap {count, plural, other {其他 # ê lâng}} ê口座有開ah",
+  "notification.annual_report.message": "Lí ê {year} #Wrapstodon teh等lí!緊來看tsit年lí佇Mastodon頂ê上精彩ê內容,kap難忘ê時刻!",
+  "notification.annual_report.view": "Kā #Wrapstodon 看māi。",
+  "notification.favourite": "{name} kah意lí ê PO文",
+  "notification.favourite.name_and_others_with_link": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>kah意lí ê PO文",
   "notification.favourite_pm": "{name} kah意lí ê私人提起",
   "notification.favourite_pm.name_and_others_with_link": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>kah意lí ê私人提起",
+  "notification.follow": "{name}跟tuè lí",
+  "notification.follow.name_and_others": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>跟tuè lí",
+  "notification.follow_request": "{name} 請求跟tuè lí",
+  "notification.follow_request.name_and_others": "{name} kap{count, plural, other {另外 # ê lâng}}請求跟tuè lí",
+  "notification.label.mention": "提起",
+  "notification.label.private_mention": "私人ê提起",
+  "notification.label.private_reply": "私人ê回應",
+  "notification.label.reply": "回應",
+  "notification.mention": "提起",
+  "notification.mentioned_you": "{name}kā lí提起",
+  "notification.moderation-warning.learn_more": "看詳細",
+  "notification.moderation_warning": "Lí有收著審核ê警告",
+  "notification.moderation_warning.action_delete_statuses": "Lí ê一寡PO文hōo lâng thâi掉ah。",
+  "notification.moderation_warning.action_disable": "Lí ê口座hōo lâng停止使用ah。",
+  "notification.moderation_warning.action_mark_statuses_as_sensitive": "Lí ê一寡PO文,hōo lâng標做敏感ê內容。",
+  "notification.moderation_warning.action_none": "Lí ê口座有收著審核ê警告。",
+  "notification_requests.edit_selection": "編輯",
+  "notification_requests.exit_selection": "做好ah",
   "search_popout.language_code": "ISO語言代碼",
   "status.translated_from_with": "用 {provider} 翻譯 {lang}"
 }
diff --git a/app/javascript/mastodon/locales/ne.json b/app/javascript/mastodon/locales/ne.json
index 6fe1330d6c..af2c922cbd 100644
--- a/app/javascript/mastodon/locales/ne.json
+++ b/app/javascript/mastodon/locales/ne.json
@@ -25,7 +25,6 @@
   "account.enable_notifications": "@{name} ले पोस्ट गर्दा मलाई सूचित गर्नुहोस्",
   "account.endorse": "प्रोफाइलमा फिचर गर्नुहोस्",
   "account.featured_tags.last_status_never": "कुनै पोस्ट छैन",
-  "account.featured_tags.title": "{name}का विशेष ह्यासट्यागहरू",
   "account.follow": "फलो गर्नुहोस",
   "account.follow_back": "फलो ब्याक गर्नुहोस्",
   "account.followers": "फलोअरहरु",
@@ -117,6 +116,8 @@
   "compose_form.publish_form": "नयाँ पोस्ट",
   "compose_form.reply": "जवाफ दिनुहोस्",
   "compose_form.save_changes": "अपडेट गर्नुहोस्",
+  "confirmation_modal.cancel": "रद्द गर्नुहोस्",
+  "confirmations.block.confirm": "ब्लक गर्नुहोस्",
   "confirmations.delete.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाईं यो पोष्ट मेटाउन चाहनुहुन्छ?",
   "confirmations.delete.title": "पोस्ट मेटाउने?",
   "confirmations.delete_list.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाईं यो सूची स्थायी रूपमा मेटाउन चाहनुहुन्छ?",
@@ -129,7 +130,10 @@
   "confirmations.follow_to_list.title": "प्रयोगकर्तालाई फलो गर्ने?",
   "confirmations.logout.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाइँ लाई लग आउट गर्न चाहनुहुन्छ?",
   "confirmations.logout.title": "लग आउट गर्ने?",
+  "confirmations.mute.confirm": "म्यूट गर्नुहोस्",
+  "confirmations.redraft.confirm": "मेटाएर पुन: ड्राफ्ट गर्नुहोस्",
   "confirmations.redraft.title": "पोस्ट मेटाएर पुन: ड्राफ्ट गर्ने?",
+  "confirmations.reply.confirm": "जवाफ दिनुहोस्",
   "confirmations.reply.message": "अहिले जवाफ दिनाले तपाईंले हाल लेखिरहनुभएको सन्देश अधिलेखन हुनेछ। के तपाईं अगाडि बढ्न चाहनुहुन्छ?",
   "confirmations.reply.title": "पोस्ट अधिलेखन गर्ने?",
   "confirmations.unfollow.confirm": "अनफलो गर्नुहोस्",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 3329878229..30f4777f61 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Profiel bewerken",
   "account.enable_notifications": "Geef een melding wanneer @{name} een bericht plaatst",
   "account.endorse": "Op profiel weergeven",
+  "account.featured": "Uitgelicht",
+  "account.featured.hashtags": "Hashtags",
+  "account.featured.posts": "Berichten",
   "account.featured_tags.last_status_at": "Laatste bericht op {date}",
   "account.featured_tags.last_status_never": "Geen berichten",
-  "account.featured_tags.title": "Uitgelichte hashtags van {name}",
   "account.follow": "Volgen",
   "account.follow_back": "Terugvolgen",
   "account.followers": "Volgers",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} bericht} other {{counter} berichten}}",
   "account.unblock": "@{name} deblokkeren",
   "account.unblock_domain": "{domain} niet langer blokkeren",
+  "account.unblock_domain_short": "Deblokkeren",
   "account.unblock_short": "Deblokkeren",
   "account.unendorse": "Niet op profiel weergeven",
   "account.unfollow": "Ontvolgen",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Zoekresultaten",
   "emoji_button.symbols": "Symbolen",
   "emoji_button.travel": "Reizen en locaties",
+  "empty_column.account_featured": "Deze lijst is leeg",
   "empty_column.account_hides_collections": "Deze gebruiker heeft ervoor gekozen deze informatie niet beschikbaar te maken",
   "empty_column.account_suspended": "Account opgeschort",
   "empty_column.account_timeline": "Hier zijn geen berichten!",
@@ -377,6 +381,8 @@
   "generic.saved": "Opgeslagen",
   "getting_started.heading": "Aan de slag",
   "hashtag.admin_moderation": "Moderatie-omgeving van #{name} openen",
+  "hashtag.browse": "Berichten met #{hashtag} bekijken",
+  "hashtag.browse_from_account": "Berichten van @{name} met #{hashtag} bekijken",
   "hashtag.column_header.tag_mode.all": "en {additional}",
   "hashtag.column_header.tag_mode.any": "of {additional}",
   "hashtag.column_header.tag_mode.none": "zonder {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} bericht} other {{counter} berichten}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} bericht} other {{counter} berichten}} vandaag",
   "hashtag.follow": "Hashtag volgen",
+  "hashtag.mute": "#{hashtag} negeren",
   "hashtag.unfollow": "Hashtag ontvolgen",
   "hashtags.and_other": "…en {count, plural, one {}other {# meer}}",
   "hints.profiles.followers_may_be_missing": "Volgers voor dit profiel kunnen ontbreken.",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "Getoonde talen voor {target} wijzigen",
   "tabs_bar.home": "Start",
   "tabs_bar.notifications": "Meldingen",
+  "terms_of_service.effective_as_of": "Van kracht met ingang van {date}",
   "terms_of_service.title": "Gebruiksvoorwaarden",
+  "terms_of_service.upcoming_changes_on": "Aankomende wijzigingen op {date}",
   "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} te gaan",
   "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} te gaan",
   "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} te gaan",
@@ -903,8 +912,12 @@
   "video.expand": "Video groter maken",
   "video.fullscreen": "Volledig scherm",
   "video.hide": "Video verbergen",
-  "video.mute": "Geluid uitschakelen",
+  "video.mute": "Dempen",
   "video.pause": "Pauze",
   "video.play": "Afspelen",
-  "video.unmute": "Geluid inschakelen"
+  "video.skip_backward": "Terugspoelen",
+  "video.skip_forward": "Vooruitspoelen",
+  "video.unmute": "Dempen opheffen",
+  "video.volume_down": "Volume omlaag",
+  "video.volume_up": "Volume omhoog"
 }
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index 95d173dca5..ab867017a9 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Rediger profil",
   "account.enable_notifications": "Varsle meg når @{name} skriv innlegg",
   "account.endorse": "Vis på profilen",
+  "account.featured": "Utvald",
+  "account.featured.hashtags": "Emneknaggar",
+  "account.featured.posts": "Innlegg",
   "account.featured_tags.last_status_at": "Sist nytta {date}",
   "account.featured_tags.last_status_never": "Ingen innlegg",
-  "account.featured_tags.title": "{name} sine framheva emneknaggar",
   "account.follow": "Fylg",
   "account.follow_back": "Fylg tilbake",
   "account.followers": "Fylgjarar",
@@ -86,6 +88,13 @@
   "alert.unexpected.message": "Det oppstod eit uventa problem.",
   "alert.unexpected.title": "Oi sann!",
   "alt_text_badge.title": "Alternativ tekst",
+  "alt_text_modal.add_alt_text": "Legg til alternativ tekst",
+  "alt_text_modal.add_text_from_image": "Legg til tekst frå biletet",
+  "alt_text_modal.cancel": "Avbryt",
+  "alt_text_modal.change_thumbnail": "Endre miniatyrbilete",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Skriv ei skildring for menneske med høyrselsnedsetjingar…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Skriv ei skildring for menneske med synsnedsetjingar…",
+  "alt_text_modal.done": "Ferdig",
   "announcement.announcement": "Kunngjering",
   "annual_report.summary.archetype.booster": "Den som jaktar på noko kult",
   "annual_report.summary.archetype.lurker": "Den som heng på hjørnet",
@@ -286,6 +295,7 @@
   "emoji_button.search_results": "Søkeresultat",
   "emoji_button.symbols": "Symbol",
   "emoji_button.travel": "Reise & stader",
+  "empty_column.account_featured": "Denne lista er tom",
   "empty_column.account_hides_collections": "Denne brukaren har valt å ikkje gjere denne informasjonen tilgjengeleg",
   "empty_column.account_suspended": "Kontoen er utestengd",
   "empty_column.account_timeline": "Ingen tut her!",
@@ -411,6 +421,8 @@
   "ignore_notifications_modal.not_followers_title": "Oversjå varsel frå folk som ikkje fylgjer deg?",
   "ignore_notifications_modal.not_following_title": "Oversjå varsel frå folk du ikkje fylgjer?",
   "ignore_notifications_modal.private_mentions_title": "Oversjå varsel frå masseutsende private omtaler?",
+  "info_button.label": "Hjelp",
+  "info_button.what_is_alt_text": "<h1>Kva er alternativ tekst?</h1> <p>Alternativ eller skildrande tekst gjev ei skildring av biletet for menneske som har synsvanskar, sein nettilkopling eller dei som ser etter ekstra innhald.</p> <p>Du gjer innhaldet ditt meir tilgjengeleg og forståeleg for alle ved å skriva klåre, presise og nøytrale alt-tekstar.</p> <ul> <li>Skriv om viktige element</li> <li>Oppsummer tekst i bilete</li> <li>Skriv vanlege setningar</li> <li>Unngå unyttige opplysingar</li> <li>Legg vekt på hovudpunkta i innhaldsrike visuelle element, som grafar eller kart</li> </ul>",
   "interaction_modal.action.favourite": "Du må favorittmerka frå kontoen din for å halda fram.",
   "interaction_modal.action.follow": "Du må fylgja frå kontoen din for å halda fram.",
   "interaction_modal.action.reblog": "Du må framheva frå kontoen din for å halda fram.",
@@ -688,7 +700,7 @@
   "poll_button.remove_poll": "Fjern rundspørjing",
   "privacy.change": "Endre personvernet på innlegg",
   "privacy.direct.long": "Alle nemnde i innlegget",
-  "privacy.direct.short": "Spesifikke folk",
+  "privacy.direct.short": "Privat omtale",
   "privacy.private.long": "Berre dei som fylgjer deg",
   "privacy.private.short": "Fylgjarar",
   "privacy.public.long": "Kven som helst på og av Mastodon",
@@ -894,8 +906,6 @@
   "video.expand": "Utvid video",
   "video.fullscreen": "Fullskjerm",
   "video.hide": "Gøym video",
-  "video.mute": "Demp lyd",
   "video.pause": "Pause",
-  "video.play": "Spel av",
-  "video.unmute": "Av-dempe lyd"
+  "video.play": "Spel av"
 }
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 7e19eb06a3..a18ccdc0dc 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -29,7 +29,6 @@
   "account.endorse": "Vis frem på profilen",
   "account.featured_tags.last_status_at": "Siste innlegg {date}",
   "account.featured_tags.last_status_never": "Ingen Innlegg",
-  "account.featured_tags.title": "{name} sine fremhevede emneknagger",
   "account.follow": "Følg",
   "account.follow_back": "Følg tilbake",
   "account.followers": "Følgere",
@@ -85,7 +84,31 @@
   "alert.rate_limited.title": "Hastighetsbegrenset",
   "alert.unexpected.message": "En uventet feil oppstod.",
   "alert.unexpected.title": "Ups!",
+  "alt_text_badge.title": "Alternativ tekst",
+  "alt_text_modal.add_alt_text": "Legg til alternativ tekst",
+  "alt_text_modal.add_text_from_image": "Legg til tekst fra bildet",
+  "alt_text_modal.cancel": "Avbryt",
+  "alt_text_modal.change_thumbnail": "Endre miniatyrbilde",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Beskriv dette for folk med hørselsproblemer…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Beskriv dette for folk med synsproblemer…",
+  "alt_text_modal.done": "Ferdig",
   "announcement.announcement": "Kunngjøring",
+  "annual_report.summary.archetype.booster": "Den iskalde jeger",
+  "annual_report.summary.archetype.lurker": "Det snikende ullteppe",
+  "annual_report.summary.archetype.oracle": "Allviteren",
+  "annual_report.summary.archetype.pollster": "Meningsmåleren",
+  "annual_report.summary.archetype.replier": "Festens midtpunkt",
+  "annual_report.summary.followers.followers": "følgere",
+  "annual_report.summary.followers.total": "{count} tilsammen",
+  "annual_report.summary.here_it_is": "Her er ditt {year} ditt sett sammlet:",
+  "annual_report.summary.highlighted_post.by_favourites": "mest likte innlegg",
+  "annual_report.summary.highlighted_post.by_reblogs": "mest opphøyde innlegg",
+  "annual_report.summary.highlighted_post.by_replies": "innlegg med de fleste tilbakemeldinger",
+  "annual_report.summary.highlighted_post.possessive": "{name}",
+  "annual_report.summary.most_used_app.most_used_app": "mest brukte applikasjoner",
+  "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest brukte evne knagg",
+  "annual_report.summary.most_used_hashtag.none": "Ingen",
+  "annual_report.summary.new_posts.new_posts": "nye innlegg",
   "attachments_list.unprocessed": "(ubehandlet)",
   "audio.hide": "Skjul lyd",
   "block_modal.remote_users_caveat": "Vi vil be serveren {domain} om å respektere din beslutning. Det er imidlertid ingen garanti at det blir overholdt, siden noen servere kan håndtere blokkeringer på forskjellig vis. Offentlige innlegg kan fortsatt være synlige for ikke-innloggede brukere.",
@@ -122,6 +145,7 @@
   "column.direct": "Private omtaler",
   "column.directory": "Bla gjennom profiler",
   "column.domain_blocks": "Skjulte domener",
+  "column.edit_list": "Rediger listen",
   "column.favourites": "Favoritter",
   "column.firehose": "Tidslinjer",
   "column.follow_requests": "Følgeforespørsler",
@@ -138,6 +162,7 @@
   "column_header.pin": "Fest",
   "column_header.show_settings": "Vis innstillinger",
   "column_header.unpin": "Løsne",
+  "column_search.cancel": "Avbryt",
   "column_subheading.settings": "Innstillinger",
   "community.column_settings.local_only": "Kun lokalt",
   "community.column_settings.media_only": "Media only",
@@ -182,6 +207,7 @@
   "confirmations.logout.confirm": "Logg ut",
   "confirmations.logout.message": "Er du sikker på at du vil logge ut?",
   "confirmations.logout.title": "Logg ut?",
+  "confirmations.missing_alt_text.secondary": "Legg ut likevel",
   "confirmations.mute.confirm": "Demp",
   "confirmations.redraft.confirm": "Slett og skriv på nytt",
   "confirmations.redraft.message": "Er du sikker på at du vil slette dette innlegget og lagre det på nytt? Favoritter og fremhevinger vil gå tapt, og svar til det originale innlegget vil bli foreldreløse.",
@@ -194,6 +220,7 @@
   "confirmations.unfollow.title": "Slutt å følge bruker?",
   "content_warning.hide": "Skjul innlegg",
   "content_warning.show": "Vis likevel",
+  "content_warning.show_more": "Vis mer",
   "conversation.delete": "Slett samtalen",
   "conversation.mark_as_read": "Marker som lest",
   "conversation.open": "Vis samtale",
@@ -368,6 +395,9 @@
   "ignore_notifications_modal.not_followers_title": "Overse varsler fra folk som ikke følger deg?",
   "ignore_notifications_modal.not_following_title": "Overse varsler fra folk du ikke følger?",
   "ignore_notifications_modal.private_mentions_title": "Overse varsler fra uoppfordrede private omtaler?",
+  "info_button.label": "Hjelp",
+  "interaction_modal.go": "Gå",
+  "interaction_modal.no_account_yet": "Har du ikke en konto ennå?",
   "interaction_modal.on_another_server": "På en annen server",
   "interaction_modal.on_this_server": "På denne serveren",
   "interaction_modal.title.favourite": "Favorittmarker innlegget til {name}",
@@ -419,13 +449,22 @@
   "link_preview.author": "Av {name}",
   "link_preview.more_from_author": "Mer fra {name}",
   "link_preview.shares": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}",
+  "lists.add_member": "Legg til",
+  "lists.add_to_list": "Legg til i listen",
+  "lists.create": "Opprett",
   "lists.delete": "Slett listen",
+  "lists.done": "Ferdig",
   "lists.edit": "Rediger listen",
+  "lists.no_results_found": "Ingen treff.",
+  "lists.remove_member": "Fjern",
   "lists.replies_policy.followed": "Enhver fulgt bruker",
   "lists.replies_policy.list": "Medlemmer i listen",
   "lists.replies_policy.none": "Ingen",
+  "lists.save": "Lagre",
+  "lists.search": "Søk",
   "load_pending": "{count, plural,one {# ny gjenstand} other {# nye gjenstander}}",
   "loading_indicator.label": "Laster…",
+  "media_gallery.hide": "Skjul",
   "moved_to_account_banner.text": "Din konto {disabledAccount} er for øyeblikket deaktivert fordi du flyttet til {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ikke varsle",
   "mute_modal.hide_options": "Skjul alternativer",
@@ -470,10 +509,20 @@
   "notification.favourite": "{name} favorittmarkerte innlegget ditt",
   "notification.follow": "{name} fulgte deg",
   "notification.follow_request": "{name} har bedt om å få følge deg",
+  "notification.label.mention": "Nevn",
+  "notification.label.reply": "Svar",
+  "notification.mention": "Nevn",
+  "notification.mentioned_you": "{name} nevnte deg",
+  "notification.moderation-warning.learn_more": "Lær mer",
   "notification.own_poll": "Avstemningen din er ferdig",
   "notification.reblog": "{name} fremhevet ditt innlegg",
+  "notification.relationships_severance_event.learn_more": "Lær mer",
   "notification.status": "{name} la nettopp ut",
   "notification.update": "{name} redigerte et innlegg",
+  "notification_requests.accept": "Aksepter",
+  "notification_requests.dismiss": "Lukk",
+  "notification_requests.edit_selection": "Redigér",
+  "notification_requests.exit_selection": "Ferdig",
   "notification_requests.minimize_banner": "Minimer banneret for filtrerte varsler",
   "notification_requests.view": "Vis varsler",
   "notifications.clear": "Fjern varsler",
@@ -486,6 +535,7 @@
   "notifications.column_settings.filter_bar.category": "Hurtigfiltreringslinje",
   "notifications.column_settings.follow": "Nye følgere:",
   "notifications.column_settings.follow_request": "Nye følgerforespørsler:",
+  "notifications.column_settings.group": "Gruppe",
   "notifications.column_settings.mention": "Nevnt:",
   "notifications.column_settings.poll": "Avstemningsresultater:",
   "notifications.column_settings.push": "Push varsler",
@@ -529,7 +579,10 @@
   "notifications_permission_banner.enable": "Skru på skrivebordsvarsler",
   "notifications_permission_banner.how_to_control": "For å motta varsler når Mastodon ikke er åpne, aktiver desktop varsler. Du kan kontrollere nøyaktig hvilke typer interaksjoner genererer skrivebordsvarsler gjennom {icon} -knappen ovenfor når de er aktivert.",
   "notifications_permission_banner.title": "Aldri gå glipp av noe",
+  "onboarding.follows.back": "Tilbake",
+  "onboarding.follows.done": "Ferdig",
   "onboarding.follows.empty": "Dessverre kan ingen resultater vises akkurat nå. Du kan prøve å bruke søk eller bla gjennom utforske-siden for å finne folk å følge, eller prøve igjen senere.",
+  "onboarding.follows.search": "Søk",
   "onboarding.profile.discoverable": "Gjør min profil synlig",
   "onboarding.profile.display_name": "Visningsnavn",
   "onboarding.profile.display_name_hint": "Ditt fulle navn eller ditt morsomme navn…",
@@ -554,7 +607,6 @@
   "poll_button.remove_poll": "Fjern avstemningen",
   "privacy.change": "Juster synlighet",
   "privacy.direct.long": "Alle nevnt i innlegget",
-  "privacy.direct.short": "Spesifikke folk",
   "privacy.private.long": "Bare følgerne dine",
   "privacy.private.short": "Følgere",
   "privacy.public.long": "Alle på og utenfor Mastodon",
@@ -622,6 +674,7 @@
   "report_notification.attached_statuses": "{count, plural,one {{count} innlegg} other {{count} innlegg}} vedlagt",
   "report_notification.categories.legal": "Juridiske",
   "report_notification.categories.other": "Annet",
+  "report_notification.categories.other_sentence": "annet",
   "report_notification.categories.spam": "Søppelpost",
   "report_notification.categories.violation": "Regelbrudd",
   "report_notification.open": "Åpne rapport",
@@ -644,6 +697,7 @@
   "search_results.accounts": "Profiler",
   "search_results.all": "Alle",
   "search_results.hashtags": "Emneknagger",
+  "search_results.no_results": "Ingen resultater.",
   "search_results.see_all": "Se alle",
   "search_results.statuses": "Innlegg",
   "server_banner.about_active_users": "Personer som har brukt denne serveren i løpet av de siste 30 dagene (aktive brukere månedlig)",
@@ -709,6 +763,7 @@
   "subscribed_languages.target": "Endre abonnerte språk for {target}",
   "tabs_bar.home": "Hjem",
   "tabs_bar.notifications": "Varslinger",
+  "terms_of_service.title": "Bruksvilkår",
   "time_remaining.days": "{number, plural,one {# dag} other {# dager}} igjen",
   "time_remaining.hours": "{number, plural, one {# time} other {# timer}} igjen",
   "time_remaining.minutes": "{number, plural, one {# minutt} other {# minutter}} igjen",
@@ -734,8 +789,6 @@
   "video.expand": "Utvid video",
   "video.fullscreen": "Fullskjerm",
   "video.hide": "Skjul video",
-  "video.mute": "Skru av lyd",
   "video.pause": "Pause",
-  "video.play": "Spill av",
-  "video.unmute": "Skru på lyd"
+  "video.play": "Spill av"
 }
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 1ab2c774c5..74ae8fad4d 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -26,7 +26,6 @@
   "account.endorse": "Mostrar pel perfil",
   "account.featured_tags.last_status_at": "Darrièra publicacion lo {date}",
   "account.featured_tags.last_status_never": "Cap de publicacion",
-  "account.featured_tags.title": "Etiquetas en avant de {name}",
   "account.follow": "Sègre",
   "account.follow_back": "Sègre en retorn",
   "account.followers": "Seguidors",
@@ -550,8 +549,6 @@
   "video.expand": "Agrandir la vidèo",
   "video.fullscreen": "Ecran complèt",
   "video.hide": "Amagar la vidèo",
-  "video.mute": "Copar lo son",
   "video.pause": "Pausa",
-  "video.play": "Lectura",
-  "video.unmute": "Restablir lo son"
+  "video.play": "Lectura"
 }
diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json
index dd5d68dc83..c294bcbddb 100644
--- a/app/javascript/mastodon/locales/pa.json
+++ b/app/javascript/mastodon/locales/pa.json
@@ -39,7 +39,7 @@
   "account.no_bio": "ਕੋਈ ਵਰਣਨ ਨਹੀਂ ਦਿੱਤਾ।",
   "account.open_original_page": "ਅਸਲ ਸਫ਼ੇ ਨੂੰ ਖੋਲ੍ਹੋ",
   "account.posts": "ਪੋਸਟਾਂ",
-  "account.posts_with_replies": "ਪੋਸਤਾਂ ਅਤੇ ਜਵਾਬ",
+  "account.posts_with_replies": "ਪੋਸਟਾਂ ਅਤੇ ਜਵਾਬ",
   "account.report": "{name} ਬਾਰੇ ਰਿਪੋਰਟ ਕਰੋ",
   "account.requested": "ਮਨਜ਼ੂਰੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ। ਫ਼ਾਲੋ ਬੇਨਤੀਆਂ ਨੂੰ ਰੱਦ ਕਰਨ ਲਈ ਕਲਿੱਕ ਕਰੋ",
   "account.requested_follow": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਨ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ",
@@ -57,6 +57,8 @@
   "admin.dashboard.retention.cohort_size": "ਨਵੇਂ ਵਰਤੋਂਕਾਰ",
   "alert.unexpected.title": "ਓਹੋ!",
   "alt_text_badge.title": "ਬਦਲੀ ਲਿਖਤ",
+  "alt_text_modal.cancel": "ਰੱਦ ਕਰੋ",
+  "alt_text_modal.done": "ਮੁਕੰਮਲ",
   "announcement.announcement": "ਹੋਕਾ",
   "annual_report.summary.followers.followers": "ਫ਼ਾਲੋਅਰ",
   "annual_report.summary.followers.total": "{count} ਕੁੱਲ",
@@ -73,6 +75,7 @@
   "block_modal.show_more": "ਵੱਧ ਦਿਖਾਓ",
   "block_modal.title": "ਵਰਤੋਂਕਾਰ ਉੱਤੇ ਪਾਬੰਦੀ ਲਾਉਣੀ ਹੈ?",
   "boost_modal.reblog": "ਪੋਸਟ ਨੂੰ ਬੂਸਟ ਕਰਨਾ ਹੈ?",
+  "bundle_column_error.copy_stacktrace": "ਗਲਤੀ ਰਿਪੋਰਟ ਨੂੰ ਕਾਪੀ ਕਰੋ",
   "bundle_column_error.error.title": "ਓਹ ਹੋ!",
   "bundle_column_error.network.title": "ਨੈੱਟਵਰਕ ਦੀ ਸਮੱਸਿਆ",
   "bundle_column_error.retry": "ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ",
@@ -106,6 +109,7 @@
   "column_header.pin": "ਟੰਗੋ",
   "column_header.show_settings": "ਸੈਟਿੰਗਾਂ ਦਿਖਾਓ",
   "column_header.unpin": "ਲਾਹੋ",
+  "column_search.cancel": "ਰੱਦ ਕਰੋ",
   "column_subheading.settings": "ਸੈਟਿੰਗਾਂ",
   "community.column_settings.local_only": "ਸਿਰਫ ਲੋਕਲ ਹੀ",
   "community.column_settings.media_only": "ਸਿਰਫ ਮੀਡੀਆ ਹੀ",
@@ -141,9 +145,12 @@
   "confirmations.delete_list.title": "ਸੂਚੀ ਨੂੰ ਹਟਾਉਣਾ ਹੈ?",
   "confirmations.discard_edit_media.confirm": "ਰੱਦ ਕਰੋ",
   "confirmations.edit.confirm": "ਸੋਧ",
+  "confirmations.follow_to_list.confirm": "ਫ਼ਾਲੋ ਕਰੋ ਅਤੇ ਲਿਸਟ 'ਚ ਜੋੜੋ",
+  "confirmations.follow_to_list.title": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਫ਼ਾਲੋ ਕਰਨਾ ਹੈ?",
   "confirmations.logout.confirm": "ਬਾਹਰ ਹੋਵੋ",
   "confirmations.logout.message": "ਕੀ ਤੁਸੀਂ ਲਾਗ ਆਉਟ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
   "confirmations.logout.title": "ਲਾਗ ਆਉਟ ਕਰਨਾ ਹੈ?",
+  "confirmations.missing_alt_text.secondary": "ਕਿਵੇਂ ਵੀ ਪੋਸਟ ਕਰੋ",
   "confirmations.mute.confirm": "ਮੌਨ ਕਰੋ",
   "confirmations.redraft.confirm": "ਹਟਾਓ ਤੇ ਮੁੜ-ਡਰਾਫਟ",
   "confirmations.reply.confirm": "ਜਵਾਬ ਦੇਵੋ",
@@ -209,9 +216,12 @@
   "filter_modal.select_filter.expired": "ਮਿਆਦ ਪੁੱਗੀ",
   "filter_modal.select_filter.prompt_new": "ਨਵੀਂ ਕੈਟਾਗਰੀ: {name}",
   "filter_modal.select_filter.search": "ਖੋਜੋ ਜਾਂ ਬਣਾਓ",
+  "filter_modal.select_filter.title": "ਇਸ ਪੋਸਟ ਨੂੰ ਫਿਲਟਰ ਕਰੋ",
+  "filter_modal.title.status": "ਇੱਕ ਪੋਸਟ ਨੂੰ ਫਿਲਟਰ ਕਰੋ",
   "firehose.all": "ਸਭ",
   "firehose.local": "ਇਹ ਸਰਵਰ",
   "firehose.remote": "ਹੋਰ ਸਰਵਰ",
+  "follow_request.authorize": "ਪਰਮਾਣਿਤ",
   "follow_request.reject": "ਰੱਦ ਕਰੋ",
   "follow_suggestions.dismiss": "ਮੁੜ ਨਾ ਵੇਖਾਓ",
   "follow_suggestions.personalized_suggestion": "ਨਿੱਜੀ ਸੁਝਾਅ",
@@ -227,6 +237,7 @@
   "footer.privacy_policy": "ਪਰਦੇਦਾਰੀ ਨੀਤੀ",
   "footer.source_code": "ਸਰੋਤ ਕੋਡ ਵੇਖੋ",
   "footer.status": "ਹਾਲਤ",
+  "footer.terms_of_service": "ਸੇਵਾ ਦੀਆਂ ਸ਼ਰਤਾਂ",
   "generic.saved": "ਸਾਂਭੀ ਗਈ",
   "getting_started.heading": "ਸ਼ੁਰੂ ਕਰੀਏ",
   "hashtag.column_header.tag_mode.all": "ਅਤੇ {additional}",
@@ -243,11 +254,15 @@
   "hints.profiles.see_more_followers": "{domain} ਉੱਤੇ ਹੋਰ ਫ਼ਾਲੋਅਰ ਵੇਖੋ",
   "hints.profiles.see_more_follows": "{domain} ਉੱਤੇ ਹੋਰ ਫ਼ਾਲੋ ਨੂੰ ਵੇਖੋ",
   "hints.profiles.see_more_posts": "{domain} ਉੱਤੇ ਹੋਰ ਪੋਸਟਾਂ ਨੂੰ ਵੇਖੋ",
+  "hints.threads.see_more": "{domain} ਤੋਂ ਹੋਰ ਜਵਾਬਾਂ ਨੂੰ ਵੇਖੋ",
   "home.column_settings.show_reblogs": "ਬੂਸਟਾਂ ਨੂੰ ਵੇਖੋ",
   "home.column_settings.show_replies": "ਜਵਾਬਾਂ ਨੂੰ ਵੇਖੋ",
   "home.hide_announcements": "ਐਲਾਨਾਂ ਨੂੰ ਓਹਲੇ ਕਰੋ",
   "home.pending_critical_update.link": "ਅੱਪਡੇਟ ਵੇਖੋ",
   "ignore_notifications_modal.ignore": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਅਣਡਿੱਠਾ ਕਰੋ",
+  "info_button.label": "ਮਦਦ",
+  "interaction_modal.go": "ਜਾਓ",
+  "interaction_modal.no_account_yet": "ਹਾਲੇ ਖਾਤਾ ਨਹੀਂ ਹੈ?",
   "interaction_modal.on_another_server": "ਵੱਖਰੇ ਸਰਵਰ ਉੱਤੇ",
   "interaction_modal.on_this_server": "ਇਸ ਸਰਵਰ ਉੱਤੇ",
   "interaction_modal.title.favourite": "{name} ਦੀ ਪੋਸਟ ਨੂੰ ਪਸੰਦ ਕਰੋ",
@@ -255,6 +270,7 @@
   "interaction_modal.title.reblog": "{name} ਦੀ ਪੋਸਟ ਨੂੰ ਬੂਸਟ ਕਰੋ",
   "interaction_modal.title.reply": "{name} ਦੀ ਪੋਸਟ ਦਾ ਜਵਾਬ ਦਿਓ",
   "interaction_modal.title.vote": "{name} ਦੀ ਚੋਣ ਵਾਸਤੇ ਵੋਟ ਪਾਓ",
+  "interaction_modal.username_prompt": "ਜਿਵੇਂ {example}",
   "intervals.full.days": "{number, plural, one {# ਦਿਨ} other {# ਦਿਨ}}",
   "intervals.full.hours": "{number, plural, one {# ਘੰਟਾ} other {# ਘੰਟੇ}}",
   "intervals.full.minutes": "{number, plural, one {# ਮਿੰਟ} other {# ਮਿੰਟ}}",
@@ -268,6 +284,7 @@
   "keyboard_shortcuts.down": "ਸੂਚੀ ਵਿੱਚ ਹੇਠਾਂ ਭੇਜੋ",
   "keyboard_shortcuts.enter": "ਪੋਸਟ ਨੂੰ ਖੋਲ੍ਹੋ",
   "keyboard_shortcuts.favourite": "ਪੋਸਟ ਨੂੰ ਪਸੰਦ ਕਰੋ",
+  "keyboard_shortcuts.favourites": "ਮਨਪਸੰਦ ਸੂਚੀ ਨੂੰ ਖੋਲ੍ਹੋ",
   "keyboard_shortcuts.federated": "",
   "keyboard_shortcuts.heading": "ਕੀਬੋਰਡ ਸ਼ਾਰਟਕੱਟ",
   "keyboard_shortcuts.home": "ਮੁੱਖ-ਸਫ਼ਾ ਟਾਈਮ-ਲਾਈਨ ਨੂੰ ਖੋਲ੍ਹੋ",
@@ -288,11 +305,13 @@
   "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toggle_sensitivity": "ਮੀਡੀਆ ਦਿਖਾਉਣ/ਲੁਕਾਉਣ ਲਈ",
   "keyboard_shortcuts.toot": "ਨਵੀਂ ਪੋਸਟ ਸ਼ੁਰੂ ਕਰੋ",
+  "keyboard_shortcuts.translate": "ਪੋਸਟ ਨੂੰ ਅਨੁਵਾਦ ਕਰਨ ਲਈ",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "ਸੂਚੀ ਵਿੱਚ ਉੱਤੇ ਭੇਜੋ",
   "lightbox.close": "ਬੰਦ ਕਰੋ",
   "lightbox.next": "ਅਗਲੀ",
   "lightbox.previous": "ਪਿਛਲੀ",
+  "limited_account_hint.action": "ਪਰੋਫਾਈਲ ਨੂੰ ਕਿਵੇਂ ਵੀ ਵੇਖਾਓ",
   "link_preview.author": "{name} ਵਲੋਂ",
   "link_preview.more_from_author": "{name} ਵਲੋਂ ਹੋਰ",
   "link_preview.shares": "{count, plural, one {{counter} ਪੋਸਟ} other {{counter} ਪੋਸਟਾਂ}}",
@@ -309,13 +328,17 @@
   "lists.list_name": "ਸੂਚੀ ਦਾ ਨਾਂ",
   "lists.new_list_name": "ਨਵੀਂ ਸੂਚੀਂ ਦਾ ਨਾਂ",
   "lists.no_lists_yet": "ਹਾਲੇ ਕੋਈ ਵੀ ਸੂਚੀ ਨਹੀਂ ਹੈ।",
+  "lists.no_members_yet": "ਹਾਲੇ ਮੈਂਬਰ ਨਹੀਂ ਹਨ।",
+  "lists.no_results_found": "ਕੋਈ ਨਤੀਜਾ ਨਹੀਂ ਮਿਲਿਆ।",
   "lists.remove_member": "ਹਟਾਓ",
   "lists.replies_policy.followed": "ਕੋਈ ਵੀ ਫ਼ਾਲੋ ਕੀਤਾ ਵਰਤੋਂਕਾਰ",
   "lists.replies_policy.list": "ਸੂਚੀ ਦੇ ਮੈਂਬਰ",
   "lists.replies_policy.none": "ਕੋਈ ਨਹੀਂ",
   "lists.save": "ਸੰਭਾਲੋ",
+  "lists.search": "ਖੋਜੋ",
   "loading_indicator.label": "ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ…",
   "media_gallery.hide": "ਲੁਕਾਓ",
+  "mute_modal.hide_from_notifications": "ਨੋਟੀਫਿਕੇਸ਼ਨਾਂ ਵਿੱਚੋਂ ਲੁਕਾਓ",
   "mute_modal.show_options": "ਚੋਣਾਂ ਨੂੰ ਵੇਖਾਓ",
   "navigation_bar.about": "ਇਸ ਬਾਰੇ",
   "navigation_bar.administration": "ਪਰਸ਼ਾਸ਼ਨ",
@@ -354,6 +377,8 @@
   "notification.mentioned_you": "{name} ਨੇ ਤੁਹਾਡਾ ਜ਼ਿਕਰ ਕੀਤਾ",
   "notification.moderation-warning.learn_more": "ਹੋਰ ਜਾਣੋ",
   "notification.moderation_warning.action_disable": "ਤੁਹਾਡੇ ਖਾਤੇ ਨੂੰਅਸਮਰੱਥ ਕੀਤਾ ਹੈ।",
+  "notification.moderation_warning.action_silence": "ਤੁਹਾਡੇ ਖਾਤੇ ਨੂੰ ਸੀਮਿਤ ਕੀਤਾ ਗਿਆ ਹੈ।",
+  "notification.moderation_warning.action_suspend": "ਤੁਹਾਡੇ ਖਾਤੇ ਨੂੰ ਮੁਅੱਤਲ ਕੀਤਾ ਗਿਆ ਹੈ।",
   "notification.reblog": "{name} boosted your status",
   "notification.relationships_severance_event.learn_more": "ਹੋਰ ਜਾਣੋ",
   "notification.status": "{name} ਨੇ ਹੁਣੇ ਪੋਸਟ ਕੀਤਾ",
@@ -393,6 +418,14 @@
   "notifications.policy.accept": "ਮਨਜ਼ੂਰ",
   "notifications.policy.accept_hint": "ਨੋਟੀਫਿਕੇਸ਼ਨਾਂ ਵਿੱਚ ਵੇਖਾਓ",
   "notifications.policy.drop": "ਅਣਡਿੱਠਾ",
+  "notifications.policy.filter": "ਫਿਲਟਰ",
+  "notifications.policy.filter_new_accounts_title": "ਨਵੇਂ ਖਾਤੇ",
+  "notifications.policy.filter_not_followers_title": "ਲੋਕ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦੇ",
+  "notifications.policy.filter_not_following_hint": "ਜਦ ਤੱਕ ਤੁਸੀਂ ਉਹਨਾਂ ਨੂੰ ਖੁਦ ਮਨਜ਼ੂਰੀ ਨਹੀਂ ਦਿੰਦੇ",
+  "notifications_permission_banner.enable": "ਡੈਸਕਟਾਪ ਸੂਚਨਾਵਾਂ ਸਮਰੱਥ ਕਰੋ",
+  "onboarding.follows.back": "ਪਿੱਛੇ",
+  "onboarding.follows.done": "ਮੁਕੰਮਲ",
+  "onboarding.follows.search": "ਖੋਜੋ",
   "onboarding.profile.note": "ਜਾਣਕਾਰੀ",
   "onboarding.profile.save_and_continue": "ਸੰਭਾਲੋ ਅਤੇ ਜਾਰੀ ਰੱਖੋ",
   "onboarding.profile.title": "ਪਰੋਫਾਈਲ ਸੈਟਅੱਪ",
@@ -403,11 +436,15 @@
   "poll.vote": "ਵੋਟ ਪਾਓ",
   "poll.voted": "ਤੁਸੀਂ ਇਸ ਜਵਾਬ ਲਈ ਵੋਟ ਕੀਤਾ",
   "privacy.change": "ਪੋਸਟ ਦੀ ਪਰਦੇਦਾਰੀ ਨੂੰ ਬਦਲੋ",
+  "privacy.direct.long": "ਪੋਸਟ ਵਿੱਚ ਜ਼ਿਕਰ ਕੀਤੇ ਹਰ ਕੋਈ",
+  "privacy.private.long": "ਸਿਰਫ਼ ਤੁਹਾਡੇ ਫ਼ਾਲੋਅਰ ਹੀ",
   "privacy.private.short": "ਫ਼ਾਲੋਅਰ",
   "privacy.public.short": "ਜਨਤਕ",
+  "privacy_policy.last_updated": "ਆਖਰੀ ਵਾਰ {date} ਨੂੰ ਅੱਪਡੇਟ ਕੀਤਾ",
   "privacy_policy.title": "ਪਰਦੇਦਾਰੀ ਨੀਤੀ",
   "recommended": "ਸਿਫ਼ਾਰਸ਼ੀ",
   "refresh": "ਤਾਜ਼ਾ ਕਰੋ",
+  "regeneration_indicator.please_stand_by": "ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ।",
   "relative_time.days": "{number}ਦਿਨ",
   "relative_time.full.days": "{number, plural, one {# ਦਿਨ} other {# ਦਿਨ}} ਪਹਿਲਾਂ",
   "relative_time.full.hours": "{number, plural, one {# ਘੰਟਾ} other {# ਘੰਟੇ}} ਪਹਿਲਾਂ",
@@ -431,9 +468,16 @@
   "report.next": "ਅਗਲੀ",
   "report.placeholder": "ਵਧੀਕ ਟਿੱਪਣੀਆਂ",
   "report.reasons.dislike": "ਮੈਨੂੰ ਇਹ ਪਸੰਦ ਨਹੀਂ ਹੈ",
+  "report.reasons.legal": "ਇਹ ਗ਼ੈਰ-ਕਨੂੰਨੀ ਹੈ",
+  "report.reasons.other": "ਇਹ ਕੁਝ ਹੋਰ ਹੈ",
   "report.reasons.spam": "ਇਹ ਸਪੈਮ ਹੈ",
+  "report.rules.subtitle": "ਲਾਗੂ ਹੋਣ ਵਾਲੇ ਸਾਰੇ ਚੁਣੋ",
+  "report.rules.title": "ਕਿਹੜੇ ਨਿਯਮਾਂ ਦਾ ਉਲੰਘਣ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ?",
+  "report.statuses.subtitle": "ਲਾਗੂ ਹੋਣ ਵਾਲੇ ਸਾਰੇ ਚੁਣੋ",
   "report.submit": "ਭੇਜੋ",
   "report.target": "{target} ਰਿਪੋਰਟ",
+  "report.thanks.title": "ਇਸ ਨੂੰ ਵੇਖਣਾ ਨਹੀਂ ਚਾਹੁੰਦੇ ਹੋ?",
+  "report.thanks.title_actionable": "ਰਿਪੋਰਟ ਕਰਨ ਲਈ ਧੰਨਵਾਦ ਹੈ। ਅਸੀਂ ਇਸ ਦੀ ਛਾਣਬੀਣ ਕਰਾਂਗੇ।",
   "report.unfollow": "@{name} ਨੂੰ ਅਣ-ਫ਼ਾਲੋ ਕਰੋ",
   "report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached",
   "report_notification.categories.legal": "ਕਨੂੰਨੀ",
@@ -444,6 +488,7 @@
   "report_notification.categories.violation": "ਨਿਯਮ ਦੀ ਉਲੰਘਣਾ",
   "report_notification.categories.violation_sentence": "ਨਿਯਮ ਦੀ ਉਲੰਘਣਾ",
   "report_notification.open": "ਰਿਪੋਰਟ ਨੂੰ ਖੋਲ੍ਹੋ",
+  "search.no_recent_searches": "ਕੋਈ ਸੱਜਰੀ ਖੋਜ ਨਹੀਂ ਹੈ",
   "search.placeholder": "ਖੋਜੋ",
   "search.quick_action.go_to_account": "ਪਰੋਫਾਈਲ {x} ਉੱਤੇ ਜਾਓ",
   "search.quick_action.go_to_hashtag": "ਹੈਸ਼ਟੈਗ {x} ਉੱਤੇ ਜਾਓ",
@@ -456,6 +501,7 @@
   "search_results.accounts": "ਪਰੋਫਾਈਲ",
   "search_results.all": "ਸਭ",
   "search_results.hashtags": "ਹੈਸ਼ਟੈਗ",
+  "search_results.no_results": "ਕੋਈ ਨਤੀਜੇ ਨਹੀਂ ਹਨ।",
   "search_results.see_all": "ਸਭ ਵੇਖੋ",
   "search_results.statuses": "ਪੋਸਟਾਂ",
   "server_banner.active_users": "ਸਰਗਰਮ ਵਰਤੋਂਕਾਰ",
@@ -478,31 +524,49 @@
   "status.load_more": "ਹੋਰ ਦਿਖਾਓ",
   "status.media.open": "ਖੋਲ੍ਹਣ ਲਈ ਕਲਿੱਕ ਕਰੋ",
   "status.media.show": "ਵੇਖਾਉਣ ਲਈ ਕਲਿੱਕ ਕਰੋ",
+  "status.media_hidden": "ਮੀਡਿਆ ਲੁਕਵਾਂ ਹੈ",
   "status.mention": "@{name} ਦਾ ਜ਼ਿਕਰ",
   "status.more": "ਹੋਰ",
+  "status.mute": "@{name} ਨੂੰ ਮੌਨ ਕਰੋ",
+  "status.mute_conversation": "ਗੱਲਬਾਤ ਨੂੰ ਮੌਨ ਕਰੋ",
   "status.open": "ਇਹ ਪੋਸਟ ਨੂੰ ਫੈਲਾਓ",
   "status.pin": "ਪਰੋਫਾਈਲ ਉੱਤੇ ਟੰਗੋ",
   "status.pinned": "ਟੰਗੀ ਹੋਈ ਪੋਸਟ",
   "status.read_more": "ਹੋਰ ਪੜ੍ਹੋ",
   "status.reblog": "ਵਧਾਓ",
+  "status.reblogged_by": "{name} ਨੇ ਬੂਸਟ ਕੀਤਾ",
   "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
+  "status.redraft": "ਹਟਾਓ ਤੇ ਮੁੜ-ਡਰਾਫਟ",
+  "status.remove_bookmark": "ਬੁੱਕਮਾਰਕ ਨੂੰ ਹਟਾਓ",
+  "status.remove_favourite": "ਮਨਪਸੰਦ ਵਿੱਚੋਂ ਹਟਾਓ",
+  "status.replied_in_thread": "ਮਾਮਲੇ ਵਿੱਚ ਜਵਾਬ ਦਿਓ",
   "status.replied_to": "{name} ਨੂੰ ਜਵਾਬ ਦਿੱਤਾ",
   "status.reply": "ਜਵਾਬ ਦੇਵੋ",
   "status.replyAll": "ਮਾਮਲੇ ਨੂੰ ਜਵਾਬ ਦਿਓ",
   "status.report": "@{name} ਦੀ ਰਿਪੋਰਟ ਕਰੋ",
   "status.sensitive_warning": "ਸੰਵੇਦਨਸ਼ੀਲ ਸਮੱਗਰੀ",
   "status.share": "ਸਾਂਝਾ ਕਰੋ",
+  "status.show_less_all": "ਸਭ ਲਈ ਘੱਟ ਵੇਖਾਓ",
+  "status.show_more_all": "ਸਭ ਲਈ ਵੱਧ ਵੇਖਾਓ",
+  "status.show_original": "ਅਸਲ ਨੂੰ ਵੇਖਾਓ",
   "status.title.with_attachments": "{user} ਨੇ {attachmentCount, plural,one {ਅਟੈਚਮੈਂਟ} other {{attachmentCount}ਅਟੈਚਮੈਂਟਾਂ}} ਪੋਸਟ ਕੀਤੀਆਂ",
   "status.translate": "ਉਲੱਥਾ ਕਰੋ",
+  "status.translated_from_with": "{provider} ਵਰਤ ਕੇ {lang} ਤੋਂ ਅਨੁਵਾਦ ਕੀਤਾ",
+  "status.uncached_media_warning": "ਝਲਕ ਮੌਜੂਦ ਨਹੀਂ ਹੈ",
   "status.unpin": "ਪਰੋਫਾਈਲ ਤੋਂ ਲਾਹੋ",
   "subscribed_languages.save": "ਤਬਦੀਲੀਆਂ ਸੰਭਾਲੋ",
   "tabs_bar.home": "ਘਰ",
   "tabs_bar.notifications": "ਸੂਚਨਾਵਾਂ",
+  "terms_of_service.title": "ਸੇਵਾ ਦੀਆਂ ਸ਼ਰਤਾਂ",
+  "time_remaining.days": "{number, plural, one {# ਦਿਨ} other {# ਦਿਨ}} ਬਾਕੀ",
+  "time_remaining.hours": "{number, plural, one {# ਘੰਟਾ} other {# ਘੰਟੇ}} ਬਾਕੀ",
+  "time_remaining.minutes": "{number, plural, one {# ਮਿੰਟ} other {# ਮਿੰਟ}} ਬਾਕੀ",
   "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}",
   "trends.trending_now": "ਹੁਣ ਰੁਝਾਨ ਵਿੱਚ",
   "units.short.billion": "{count}ਿਬ",
   "units.short.million": "{count}ਮਿ",
   "units.short.thousand": "{count}ਹਜ਼ਾਰ",
+  "upload_button.label": "ਚਿੱਤਰ, ਵੀਡੀਓ ਜਾਂ ਆਡੀਓ ਫਾਇਲ ਨੂੰ ਜੋੜੋ",
   "upload_form.edit": "ਸੋਧ",
   "upload_progress.label": "ਅੱਪਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ...",
   "upload_progress.processing": "ਕਾਰਵਾਈ ਚੱਲ ਰਹੀ ਹੈ…",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 8e3b8a26fa..ed827bcf29 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -11,7 +11,7 @@
   "about.not_available": "Ta informacja nie została udostępniona na tym serwerze.",
   "about.powered_by": "Zdecentralizowane media społecznościowe napędzane przez {mastodon}",
   "about.rules": "Regulamin serwera",
-  "account.account_note_header": "Twoja notatka",
+  "account.account_note_header": "Notatka",
   "account.add_or_remove_from_list": "Dodaj lub usuń z list",
   "account.badges.bot": "Bot",
   "account.badges.group": "Grupa",
@@ -19,9 +19,9 @@
   "account.block_domain": "Blokuj wszystko z {domain}",
   "account.block_short": "Zablokuj",
   "account.blocked": "Zablokowany(-a)",
-  "account.cancel_follow_request": "Wycofaj żądanie obserwowania",
-  "account.copy": "Skopiuj odnośnik do profilu",
-  "account.direct": "Prywatna wzmianka @{name}",
+  "account.cancel_follow_request": "Nie obserwuj",
+  "account.copy": "Skopiuj link do profilu",
+  "account.direct": "Napisz bezpośrednio do @{name}",
   "account.disable_notifications": "Przestań powiadamiać mnie o wpisach @{name}",
   "account.domain_blocked": "Ukryto domenę",
   "account.edit_profile": "Edytuj profil",
@@ -29,7 +29,6 @@
   "account.endorse": "Wyróżnij na profilu",
   "account.featured_tags.last_status_at": "Ostatni post {date}",
   "account.featured_tags.last_status_never": "Brak postów",
-  "account.featured_tags.title": "Polecane hasztagi {name}",
   "account.follow": "Obserwuj",
   "account.follow_back": "Również obserwuj",
   "account.followers": "Obserwujący",
@@ -52,46 +51,46 @@
   "account.mute_notifications_short": "Wycisz powiadomienia",
   "account.mute_short": "Wycisz",
   "account.muted": "Wyciszony",
-  "account.mutual": "Przyjaciele",
+  "account.mutual": "Znajomi",
   "account.no_bio": "Brak opisu.",
   "account.open_original_page": "Otwórz stronę oryginalną",
   "account.posts": "Wpisy",
   "account.posts_with_replies": "Wpisy i odpowiedzi",
   "account.report": "Zgłoś @{name}",
   "account.requested": "Oczekująca prośba, kliknij aby anulować",
-  "account.requested_follow": "{name} chce zaobserwować twój profil",
+  "account.requested_follow": "{name} chce cię zaobserwować",
   "account.share": "Udostępnij profil @{name}",
   "account.show_reblogs": "Pokazuj podbicia od @{name}",
   "account.statuses_counter": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}",
   "account.unblock": "Odblokuj @{name}",
   "account.unblock_domain": "Odblokuj domenę {domain}",
   "account.unblock_short": "Odblokuj",
-  "account.unendorse": "Przestań polecać",
-  "account.unfollow": "Przestań obserwować",
-  "account.unmute": "Cofnij wyciszenie @{name}",
-  "account.unmute_notifications_short": "Wyłącz wyciszenie powiadomień",
-  "account.unmute_short": "Włącz dźwięki",
-  "account_note.placeholder": "Naciśnij aby dodać notatkę",
+  "account.unendorse": "Nie wyświetlaj w profilu",
+  "account.unfollow": "Nie obserwuj",
+  "account.unmute": "Nie wyciszaj @{name}",
+  "account.unmute_notifications_short": "Nie wyciszaj powiadomień",
+  "account.unmute_short": "Nie wyciszaj",
+  "account_note.placeholder": "Kliknij, aby dodać notatkę",
   "admin.dashboard.daily_retention": "Wskaźnik utrzymania użytkowników po dniach od rejestracji",
   "admin.dashboard.monthly_retention": "Wskaźnik utrzymania użytkowników po miesiącach od rejestracji",
   "admin.dashboard.retention.average": "Średnia",
   "admin.dashboard.retention.cohort": "Miesiąc rejestracji",
   "admin.dashboard.retention.cohort_size": "Nowi użytkownicy",
-  "admin.impact_report.instance_accounts": "Usuniętych profili kont",
+  "admin.impact_report.instance_accounts": "Profile kont, które zostaną usunięte",
   "admin.impact_report.instance_followers": "Obserwujący, których straciliby nasi użytkownicy",
   "admin.impact_report.instance_follows": "Obserwujący, których straciliby ich użytkownicy",
-  "admin.impact_report.title": "Podsumowanie wpływu",
+  "admin.impact_report.title": "Podsumowanie zmian",
   "alert.rate_limited.message": "Spróbuj ponownie po {retry_time, time, medium}.",
   "alert.rate_limited.title": "Ograniczenie liczby zapytań",
   "alert.unexpected.message": "Wystąpił nieoczekiwany błąd.",
   "alert.unexpected.title": "Ups!",
   "alt_text_badge.title": "Tekst alternatywny",
   "alt_text_modal.add_alt_text": "Dodaj tekst alternatywny",
-  "alt_text_modal.add_text_from_image": "Dodaj tekst z obrazu",
+  "alt_text_modal.add_text_from_image": "Dodaj tekst ze zdjęcia",
   "alt_text_modal.cancel": "Anuluj",
   "alt_text_modal.change_thumbnail": "Zmień miniaturę",
-  "alt_text_modal.describe_for_people_with_hearing_impairments": "Opisz to dla osób z wadą słuchu…",
-  "alt_text_modal.describe_for_people_with_visual_impairments": "Opisz to dla osób z wadą wzroku…",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Opisz to dla osób niedosłyszących…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Opisz to dla osób niedowidzących…",
   "alt_text_modal.done": "Gotowe",
   "announcement.announcement": "Ogłoszenie",
   "annual_report.summary.archetype.booster": "Łowca treści",
@@ -102,41 +101,41 @@
   "annual_report.summary.followers.followers": "obserwujących",
   "annual_report.summary.followers.total": "łącznie {count}",
   "annual_report.summary.here_it_is": "Oto przegląd Twojego {year} roku:",
-  "annual_report.summary.highlighted_post.by_favourites": "najbardziej ulubiony wpis",
-  "annual_report.summary.highlighted_post.by_reblogs": "najbardziej promowany wpis",
-  "annual_report.summary.highlighted_post.by_replies": "wpis z największą liczbą odpowiedzi",
+  "annual_report.summary.highlighted_post.by_favourites": "najbardziej lubiany wpis",
+  "annual_report.summary.highlighted_post.by_reblogs": "najczęściej podbijany wpis",
+  "annual_report.summary.highlighted_post.by_replies": "wpis z największą liczbą komentarzy",
   "annual_report.summary.highlighted_post.possessive": "{name}",
   "annual_report.summary.most_used_app.most_used_app": "najczęściej używana aplikacja",
   "annual_report.summary.most_used_hashtag.most_used_hashtag": "najczęściej używany hashtag",
   "annual_report.summary.most_used_hashtag.none": "Brak",
   "annual_report.summary.new_posts.new_posts": "nowe wpisy",
-  "annual_report.summary.percentile.text": "<topLabel>Plasuje Cię w czołówce</topLabel><percentage></percentage><bottomLabel> użytkowników {domain}.</bottomLabel>",
+  "annual_report.summary.percentile.text": "<topLabel>To plasuje cię w czołówce</topLabel><percentage></percentage><bottomLabel> użytkowników {domain}.</bottomLabel>",
   "annual_report.summary.percentile.we_wont_tell_bernie": "Nie powiemy Berniemu.",
   "annual_report.summary.thanks": "Dziękujemy, że jesteś częścią Mastodona!",
   "attachments_list.unprocessed": "(nieprzetworzone)",
   "audio.hide": "Ukryj dźwięk",
-  "block_modal.remote_users_caveat": "Poprosimy serwer {domain} o uszanowanie twojej decyzji. Zgodność nie jest jednak gwarantowana, bo niektóre serwery mogą inaczej obsługiwać blokowanie. Wpisy publiczne mogą być widoczne dla niezalogowanych użytkowników.",
+  "block_modal.remote_users_caveat": "Poprosimy serwer {domain} o uszanowanie twojej decyzji. Nie jest to jednak gwarantowane, bo niektóre serwery mogą obsługiwać blokady w inny sposób. Publiczne wpisy mogą być nadal widoczne dla niezalogowanych użytkowników.",
   "block_modal.show_less": "Pokaż mniej",
   "block_modal.show_more": "Pokaż więcej",
-  "block_modal.they_cant_mention": "Użytkownik nie może Cię obserwować ani dodawać wzmianek o Tobie.",
-  "block_modal.they_cant_see_posts": "Użytkownik nie będzie widzieć Twoich wpisów, a Ty jego.",
-  "block_modal.they_will_know": "Użytkownik będzie wiedział, że jest zablokowany.",
-  "block_modal.title": "Zablokować użytkownika?",
-  "block_modal.you_wont_see_mentions": "Nie zobaczysz wpisów, które wspominają tego użytkownika.",
-  "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
+  "block_modal.they_cant_mention": "Nie może cię wzmiankować ani obserwować.",
+  "block_modal.they_cant_see_posts": "Nie zobaczycie wzajemnie swoich wpisów.",
+  "block_modal.they_will_know": "Zobaczy informację o blokadzie.",
+  "block_modal.title": "Zablokować?",
+  "block_modal.you_wont_see_mentions": "Nie zobaczysz wpisów, które zawierają wzmianki o tej osobie.",
+  "boost_modal.combo": "Możesz kliknąć {combo}, aby pominąć tę czynność następnym razem",
   "boost_modal.reblog": "Podbić wpis?",
   "boost_modal.undo_reblog": "Cofnąć podbicie?",
   "bundle_column_error.copy_stacktrace": "Skopiuj raport o błędzie",
-  "bundle_column_error.error.body": "Nie można zrenderować żądanej strony. Może to być spowodowane błędem w naszym kodzie lub problemami z kompatybilnością przeglądarki.",
+  "bundle_column_error.error.body": "Nie udało się wyświetlić tej strony. Może to być spowodowane błędem w naszym kodzie lub niezgodnością przeglądarki.",
   "bundle_column_error.error.title": "O nie!",
-  "bundle_column_error.network.body": "Wystąpił błąd podczas próby załadowania tej strony. Może to być spowodowane tymczasowym problemem z połączeniem z internetem lub serwerem.",
+  "bundle_column_error.network.body": "Wystąpił błąd podczas próby wczytania tej strony. Może to być spowodowane tymczasowym problemem z połączeniem internetowym lub serwerem.",
   "bundle_column_error.network.title": "Błąd sieci",
   "bundle_column_error.retry": "Spróbuj ponownie",
   "bundle_column_error.return": "Wróć do strony głównej",
-  "bundle_column_error.routing.body": "Żądana strona nie została znaleziona. Czy na pewno adres URL w pasku adresu jest poprawny?",
+  "bundle_column_error.routing.body": "Nie można odnaleźć tej strony. Czy URL w pasku adresu na pewno jest prawidłowy?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Zamknij",
-  "bundle_modal_error.message": "Coś poszło nie tak podczas ładowania tego ekranu.",
+  "bundle_modal_error.message": "Coś poszło nie tak podczas wczytywania tego ekranu.",
   "bundle_modal_error.retry": "Spróbuj ponownie",
   "closed_registrations.other_server_instructions": "Ponieważ Mastodon jest zdecentralizowany, możesz założyć konto na innym serwerze i wciąż mieć możliwość wchodzenia w interakcję z tym serwerem.",
   "closed_registrations_modal.description": "Opcja tworzenia kont na {domain} jest aktualnie niedostępna, ale miej na uwadze to, że nie musisz mieć konta konkretnie na {domain} by używać Mastodona.",
@@ -144,21 +143,21 @@
   "closed_registrations_modal.preamble": "Mastodon jest zdecentralizowany, więc bez względu na to, gdzie się zarejestrujesz, będziesz w stanie obserwować i wchodzić w interakcje z innymi osobami na tym serwerze. Możesz nawet uruchomić własny serwer!",
   "closed_registrations_modal.title": "Rejestracja na Mastodonie",
   "column.about": "O serwerze",
-  "column.blocks": "Zablokowani użytkownicy",
+  "column.blocks": "Zablokowani",
   "column.bookmarks": "Zakładki",
   "column.community": "Lokalna oś czasu",
   "column.create_list": "Utwórz listę",
-  "column.direct": "Prywatne wzmianki",
+  "column.direct": "Wzmianki bezpośrednie",
   "column.directory": "Przeglądaj profile",
-  "column.domain_blocks": "Ukryte domeny",
+  "column.domain_blocks": "Zablokowane domeny",
   "column.edit_list": "Edytuj listę",
   "column.favourites": "Ulubione",
-  "column.firehose": "Kanały na żywo",
+  "column.firehose": "Aktualności",
   "column.follow_requests": "Prośby o obserwację",
   "column.home": "Strona główna",
-  "column.list_members": "Zarządzaj członkami listy",
+  "column.list_members": "Zarządzaj osobami na liście",
   "column.lists": "Listy",
-  "column.mutes": "Wyciszeni użytkownicy",
+  "column.mutes": "Wyciszeni",
   "column.notifications": "Powiadomienia",
   "column.pins": "Przypięte wpisy",
   "column.public": "Globalna oś czasu",
@@ -168,33 +167,33 @@
   "column_header.moveRight_settings": "Przesuń kolumnę w prawo",
   "column_header.pin": "Przypnij",
   "column_header.show_settings": "Pokaż ustawienia",
-  "column_header.unpin": "Cofnij przypięcie",
+  "column_header.unpin": "Odepnij",
   "column_search.cancel": "Anuluj",
   "column_subheading.settings": "Ustawienia",
-  "community.column_settings.local_only": "Tylko Lokalne",
+  "community.column_settings.local_only": "Tylko lokalne",
   "community.column_settings.media_only": "Tylko multimedia",
-  "community.column_settings.remote_only": "Tylko Zdalne",
+  "community.column_settings.remote_only": "Tylko zdalne",
   "compose.language.change": "Zmień język",
   "compose.language.search": "Szukaj języków...",
-  "compose.published.body": "Opublikowano post.",
+  "compose.published.body": "Wpis został opublikowany.",
   "compose.published.open": "Otwórz",
-  "compose.saved.body": "Post zapisany.",
+  "compose.saved.body": "Wpis został zapisany.",
   "compose_form.direct_message_warning_learn_more": "Dowiedz się więcej",
-  "compose_form.encryption_warning": "Posty na Mastodon nie są szyfrowane end-to-end. Nie udostępniaj żadnych wrażliwych informacji przez Mastodon.",
-  "compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hasztagami, ponieważ jest oznaczony jako niepubliczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hasztagów.",
-  "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię obserwuje, może wyświetlać Twoje wpisy przeznaczone tylko dla obserwujących.",
+  "compose_form.encryption_warning": "Wpisy na Mastodon nie są szyfrowane end-to-end. Nie udostępniaj żadnych poufnych informacji za pośrednictwem Mastodon.",
+  "compose_form.hashtag_warning": "Ten wpis nie będzie wyświetlany pod żadnym hashtagiem, bo nie jest publiczny. Tylko publiczne wpisy mogą być wyszukiwane po hashtagach.",
+  "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy może cię obserwować, aby zobaczyć twoje wpisy tylko dla obserwujących.",
   "compose_form.lock_disclaimer.lock": "zablokowane",
-  "compose_form.placeholder": "Co chodzi ci po głowie?",
-  "compose_form.poll.duration": "Czas trwania głosowania",
-  "compose_form.poll.multiple": "Wielokrotny wybór",
+  "compose_form.placeholder": "Co ci chodzi po głowie?",
+  "compose_form.poll.duration": "Czas trwania ankiety",
+  "compose_form.poll.multiple": "Możliwość wielokrotnego wyboru",
   "compose_form.poll.option_placeholder": "Opcja {number}",
-  "compose_form.poll.single": "Jednokrotny wybór",
-  "compose_form.poll.switch_to_multiple": "Pozwól na wybranie wielu opcji",
-  "compose_form.poll.switch_to_single": "Pozwól na wybranie tylko jednej opcji",
+  "compose_form.poll.single": "Maksymalnie jedna odpowiedź",
+  "compose_form.poll.switch_to_multiple": "Pozwól na zaznaczenie kilku odpowiedzi",
+  "compose_form.poll.switch_to_single": "Pozwól na zaznaczenie tylko jednej odpowiedzi",
   "compose_form.poll.type": "Styl",
   "compose_form.publish": "Opublikuj",
-  "compose_form.publish_form": "Opublikuj",
-  "compose_form.reply": "Odpowiedz",
+  "compose_form.publish_form": "Nowy wpis",
+  "compose_form.reply": "Skomentuj",
   "compose_form.save_changes": "Aktualizuj",
   "compose_form.spoiler.marked": "Usuń ostrzeżenie o treści",
   "compose_form.spoiler.unmarked": "Dodaj ostrzeżenie o treści",
@@ -205,75 +204,79 @@
   "confirmations.delete.message": "Czy na pewno chcesz usunąć ten wpis?",
   "confirmations.delete.title": "Usunąć wpis?",
   "confirmations.delete_list.confirm": "Usuń",
-  "confirmations.delete_list.message": "Czy na pewno chcesz bezpowrotnie usunąć tą listę?",
+  "confirmations.delete_list.message": "Czy na pewno chcesz trwale usunąć tę listę?",
   "confirmations.delete_list.title": "Usunąć listę?",
   "confirmations.discard_edit_media.confirm": "Odrzuć",
   "confirmations.discard_edit_media.message": "Masz niezapisane zmiany w opisie lub podglądzie, odrzucić je mimo to?",
   "confirmations.edit.confirm": "Edytuj",
   "confirmations.edit.message": "Edytowanie wpisu nadpisze wiadomość, którą obecnie piszesz. Czy na pewno chcesz to zrobić?",
-  "confirmations.edit.title": "Nadpisać wpis?",
+  "confirmations.edit.title": "Zastąpić wpis?",
   "confirmations.follow_to_list.confirm": "Zaobserwuj i dodaj do listy",
-  "confirmations.follow_to_list.message": "Musisz obserwować {name} by dodać to konto do listy.",
-  "confirmations.follow_to_list.title": "Zaobserwować użytkownika?",
+  "confirmations.follow_to_list.message": "Musisz obserwować {name}, aby dodać do listy.",
+  "confirmations.follow_to_list.title": "Zaobserwować?",
   "confirmations.logout.confirm": "Wyloguj",
   "confirmations.logout.message": "Czy na pewno chcesz się wylogować?",
   "confirmations.logout.title": "Wylogować?",
+  "confirmations.missing_alt_text.confirm": "Dodaj opis pomocniczy",
+  "confirmations.missing_alt_text.message": "Twój wpis zawiera multimedia bez tekstu alternatywnego. Dodanie opisów pomaga zwiększyć dostępność tych treści dla większej liczby osób.",
+  "confirmations.missing_alt_text.secondary": "Opublikuj mimo to ",
+  "confirmations.missing_alt_text.title": "Dodać tekst pomocniczy?",
   "confirmations.mute.confirm": "Wycisz",
-  "confirmations.redraft.confirm": "Usuń i przeredaguj",
-  "confirmations.redraft.message": "Czy na pewno chcesz usunąć i przeredagować ten wpis? Polubienia i podbicia zostaną utracone, a odpowiedzi do oryginalnego wpisu zostaną osierocone.",
-  "confirmations.redraft.title": "Usunąć i przeredagować wpis?",
-  "confirmations.reply.confirm": "Odpowiedz",
-  "confirmations.reply.message": "W ten sposób utracisz wpis który obecnie tworzysz. Czy na pewno chcesz to zrobić?",
-  "confirmations.reply.title": "Nadpisać wpis?",
-  "confirmations.unfollow.confirm": "Przestań obserwować",
-  "confirmations.unfollow.message": "Czy na pewno zamierzasz przestać obserwować {name}?",
-  "confirmations.unfollow.title": "Przestać obserwować?",
+  "confirmations.redraft.confirm": "Usuń i popraw",
+  "confirmations.redraft.message": "Czy na pewno chcesz usunąć i poprawić ten wpis? Polubienia, podbicia i komentarze pierwotnego wpisu zostaną utracone.",
+  "confirmations.redraft.title": "Usunąć i poprawić wpis?",
+  "confirmations.reply.confirm": "Skomentuj",
+  "confirmations.reply.message": "W ten sposób utracisz wpis, który teraz tworzysz. Czy na pewno chcesz to zrobić?",
+  "confirmations.reply.title": "Zastąpić wpis?",
+  "confirmations.unfollow.confirm": "Nie obserwuj",
+  "confirmations.unfollow.message": "Czy na pewno nie chcesz obserwować {name}?",
+  "confirmations.unfollow.title": "Cofnąć obserwację?",
   "content_warning.hide": "Ukryj wpis",
   "content_warning.show": "Pokaż mimo to",
-  "content_warning.show_more": "Rozwiń",
-  "conversation.delete": "Usuń konwersację",
+  "content_warning.show_more": "Pokaż więcej",
+  "conversation.delete": "Usuń rozmowę",
   "conversation.mark_as_read": "Oznacz jako przeczytane",
-  "conversation.open": "Zobacz konwersację",
+  "conversation.open": "Zobacz rozmowę",
   "conversation.with": "Z {names}",
   "copy_icon_button.copied": "Skopiowano do schowka",
   "copypaste.copied": "Skopiowano",
   "copypaste.copy_to_clipboard": "Skopiuj do schowka",
   "directory.federated": "Ze znanego fediwersum",
   "directory.local": "Tylko z {domain}",
-  "directory.new_arrivals": "Nowości",
-  "directory.recently_active": "Ostatnio aktywne",
+  "directory.new_arrivals": "Nowo przybyli",
+  "directory.recently_active": "Ostatnio aktywni",
   "disabled_account_banner.account_settings": "Ustawienia konta",
   "disabled_account_banner.text": "Twoje konto {disabledAccount} jest obecnie wyłączone.",
-  "dismissable_banner.community_timeline": "To są najnowsze wpisy publiczne od osób, które mają założone konta na {domain}.",
-  "dismissable_banner.dismiss": "Schowaj",
-  "dismissable_banner.explore_links": "Te wiadomości obecnie są komentowane przez osoby z tego serwera i pozostałych w fediwersum.",
-  "dismissable_banner.explore_statuses": "Obecnie te wpisy z tego serwera i pozostałych serwerów w fediwersum zyskują popularność na tym serwerze.",
-  "dismissable_banner.explore_tags": "Te hasztagi obecnie zyskują popularność wśród osób z tego serwera i pozostałych w fediwersum.",
-  "dismissable_banner.public_timeline": "Są to najnowsze publiczne wpisy osób w fediwersum, które obserwują ludzie w serwisie {domain}.",
+  "dismissable_banner.community_timeline": "To są najnowsze publiczne wpisy osób, które są na {domain}.",
+  "dismissable_banner.dismiss": "Odrzuć",
+  "dismissable_banner.explore_links": "To są dzisiejsze najpopularniejsze wiadomości w fediwerum. Aktualności publikowane przez więcej różnych osób znajdują się wyżej w rankingu.",
+  "dismissable_banner.explore_statuses": "Te wpisy z całego fediwersum zyskują dziś na popularności. Nowsze wpisy z większą liczbą podbić i polubień są wyżej w rankingu.",
+  "dismissable_banner.explore_tags": "Te hashtagi zyskują dziś na popularności w fediwersum. Hashtagi, które są używane przez więcej różnych osób, są wyżej w rankingu.",
+  "dismissable_banner.public_timeline": "To są najnowsze publiczne wpisy w fediwersum od osób obserwowanych przez użytkowników {domain}.",
   "domain_block_modal.block": "Blokuj serwer",
   "domain_block_modal.block_account_instead": "Zamiast tego zablokuj @{name}",
-  "domain_block_modal.they_can_interact_with_old_posts": "Ludzie z tego serwera mogą wchodzić w interakcje z Twoimi starymi wpisami.",
-  "domain_block_modal.they_cant_follow": "Nikt z tego serwera nie może Cię obserwować.",
-  "domain_block_modal.they_wont_know": "Użytkownik nie dowie się, że został zablokowany.",
+  "domain_block_modal.they_can_interact_with_old_posts": "Osoby z tego serwera mogą wchodzić w interakcje z twoimi starymi wpisami.",
+  "domain_block_modal.they_cant_follow": "Nikt z tego serwera nie może cię obserwować.",
+  "domain_block_modal.they_wont_know": "Nie będą wiedzieć, że zostali zablokowani.",
   "domain_block_modal.title": "Zablokować domenę?",
   "domain_block_modal.you_will_lose_num_followers": "Utracisz {followersCount, plural, one {jednego obserwującego} other {{followersCountDisplay} obserwujących}} i {followingCount, plural, one {jedną osobę którą obserwujesz} few {{followingCountDisplay} osoby które obserwujesz} other {{followingCountDisplay} osób które obserwujesz}}.",
-  "domain_block_modal.you_will_lose_relationships": "Utracisz wszystkich obserwujących z tego serwera i wszystkie osoby które obserwujesz na tym serwerze.",
-  "domain_block_modal.you_wont_see_posts": "Nie zobaczysz postów ani powiadomień od użytkowników na tym serwerze.",
-  "domain_pill.activitypub_lets_connect": "Pozwala połączyć się z ludźmi na Mastodonie, jak i na innych serwisach społecznościowych.",
-  "domain_pill.activitypub_like_language": "ActivityPub jest językiem używanym przez Mastodon do wymiany danych z innymi serwisami społecznościowymi.",
+  "domain_block_modal.you_will_lose_relationships": "Utracisz wszystkich obserwujących i obserwowanych z tego serwera.",
+  "domain_block_modal.you_wont_see_posts": "Nie zobaczysz wpisów ani powiadomień od osób z tego serwera.",
+  "domain_pill.activitypub_lets_connect": "Umożliwia komunikację i interakcję z innymi nie tylko na Mastodon, ale także w innych aplikacjach.",
+  "domain_pill.activitypub_like_language": "ActivityPub jest jak język, którym Mastodon komunikuje się z innymi sieciami społecznościowymi.",
   "domain_pill.server": "Serwer",
-  "domain_pill.their_handle": "Uchwyt:",
-  "domain_pill.their_server": "Cyfrowy dom, w którym znajdują się wszystkie wpisy.",
+  "domain_pill.their_handle": "Nazwa:",
+  "domain_pill.their_server": "Cyfrowy dom wszystkich wpisów tej osoby.",
   "domain_pill.their_username": "Unikalny identyfikator na serwerze. Możliwe jest znalezienie użytkowników o tej samej nazwie użytkownika na różnych serwerach.",
   "domain_pill.username": "Nazwa użytkownika",
-  "domain_pill.whats_in_a_handle": "Co zawiera uchwyt użytkownika?",
-  "domain_pill.who_they_are": "Ponieważ uchwyty mówią kto jest kim i gdzie się znajduje, możesz wchodzić w interakcje z ludźmi korzystającymi z <button>serwisów opartych o ActivityPub</button>.",
-  "domain_pill.who_you_are": "Ponieważ Twój uchwyt mówi kim jesteś i gdzie się znajdujesz, inni mogą wchodzić z Tobą w interakcje korzystając z <button>serwisów opartych o ActivityPub</button>.",
-  "domain_pill.your_handle": "Twój uchwyt:",
-  "domain_pill.your_server": "Twój cyfrowy dom, w którym żyją wszystkie Twoje wpisy. Nie lubisz tego? Zmień serwer w dowolnym momencie i przenieś swoich obserwujących.",
-  "domain_pill.your_username": "Twój unikalny identyfikator na tym serwerze. Użytkownicy o tej samej nazwie mogą współistnieć na różnych serwerach.",
-  "embed.instructions": "Osadź ten wpis na swojej stronie wklejając poniższy kod.",
-  "embed.preview": "Będzie to wyglądać tak:",
+  "domain_pill.whats_in_a_handle": "Z czego składa się nazwa?",
+  "domain_pill.who_they_are": "Dzięki temu, że nazwy wskazują, kim ktoś jest i gdzie się znajduje, możesz wchodzić w interakcje z innymi z różnych <button>sieci społecznościowych opartych na ActivityPub</button>.",
+  "domain_pill.who_you_are": "Dzięki temu, że twoja nazwa wskazuje, kim jesteś i gdzie się znajdujesz, inni mogą wchodzić z tobą w interakcje w różnych <button>sieciach społecznościowych opartych na ActivityPub</button>.",
+  "domain_pill.your_handle": "Twoja nazwa:",
+  "domain_pill.your_server": "Twój cyfrowy dom wszystkich twoich wpisów. Nie podoba ci się ten serwer? Przenieś się na inny w dowolnym momencie i zabierz ze sobą swoich obserwujących.",
+  "domain_pill.your_username": "Twój unikalny identyfikator na tym serwerze. Możliwe jest znalezienie osób z tą samą nazwą na innych serwerach.",
+  "embed.instructions": "Umieść ten wpis na swojej stronie, kopiując poniższy kod.",
+  "embed.preview": "Tak to będzie wyglądać:",
   "emoji_button.activity": "Aktywność",
   "emoji_button.clear": "Wyczyść",
   "emoji_button.custom": "Niestandardowe",
@@ -289,189 +292,191 @@
   "emoji_button.search_results": "Wyniki wyszukiwania",
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Podróże i miejsca",
-  "empty_column.account_hides_collections": "Użytkownik postanowił nie udostępniać tych informacji",
+  "empty_column.account_hides_collections": "Ta osoba postanowiła nie udostępniać tych informacji",
   "empty_column.account_suspended": "Konto zawieszone",
-  "empty_column.account_timeline": "Brak wpisów tutaj!",
+  "empty_column.account_timeline": "Brak wpisów!",
   "empty_column.account_unavailable": "Profil niedostępny",
-  "empty_column.blocks": "Nie zablokowałeś(-aś) jeszcze żadnego użytkownika.",
-  "empty_column.bookmarked_statuses": "Nie dodałeś(-aś) żadnego wpisu do zakładek. Kiedy to zrobisz, pojawi się on tutaj.",
-  "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
-  "empty_column.direct": "Nie masz jeszcze żadnych prywatnych wzmianek. Kiedy je wyślesz lub otrzymasz, pojawią się tutaj.",
+  "empty_column.blocks": "Nie zablokowano jeszcze żadnych użytkowników.",
+  "empty_column.bookmarked_statuses": "Nie dodano jeszcze żadnego wpisu do zakładek. Gdy to zrobisz, pojawi się tutaj.",
+  "empty_column.community": "Lokalna oś czasu jest pusta. Opublikuj coś, by ruszyć z kopyta!",
+  "empty_column.direct": "Nie ma tu jeszcze żadnych wzmianek bezpośrednich. Gdy je wyślesz lub otrzymasz, pojawią się tutaj.",
   "empty_column.domain_blocks": "Brak zablokowanych domen.",
-  "empty_column.explore_statuses": "Nic nie jest w tej chwili popularne. Sprawdź później!",
-  "empty_column.favourited_statuses": "Nie dodałeś(-aś) żadnego wpisu do ulubionych. Kiedy to zrobisz, pojawi się on tutaj.",
-  "empty_column.favourites": "Nikt nie dodał tego wpisu do ulubionych. Gdy ktoś to zrobi, pojawi się tutaj.",
-  "empty_column.follow_requests": "Nie masz żadnych próśb o możliwość obserwacji. Kiedy ktoś utworzy ją, pojawi się tutaj.",
-  "empty_column.followed_tags": "Nie obserwujesz jeszcze żadnych hashtagów. Kiedy to zrobisz, pojawią się one tutaj.",
-  "empty_column.hashtag": "Nie ma wpisów oznaczonych tym hasztagiem. Możesz napisać pierwszy(-a).",
-  "empty_column.home": "Nie obserwujesz nikogo. Odwiedź globalną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
-  "empty_column.list": "Nie ma nic na tej liście. Kiedy członkowie listy dodadzą nowe wpisy, pojawia się one tutaj.",
-  "empty_column.mutes": "Nie wyciszyłeś(-aś) jeszcze żadnego użytkownika.",
-  "empty_column.notification_requests": "To wszystko – kiedy otrzymasz nowe powiadomienia, pokażą się tutaj zgodnie z twoimi ustawieniami.",
-  "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
-  "empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych serwerów, aby to wyświetlić",
-  "error.unexpected_crash.explanation": "W związku z błędem w naszym kodzie lub braku kompatybilności przeglądarki, ta strona nie może być poprawnie wyświetlona.",
-  "error.unexpected_crash.explanation_addons": "Ta strona nie mogła zostać poprawnie wyświetlona. Może to być spowodowane dodatkiem do przeglądarki lub narzędziem do automatycznego tłumaczenia.",
-  "error.unexpected_crash.next_steps": "Spróbuj odświeżyć stronę. Jeśli to nie pomoże, wciąż jesteś w stanie używać Mastodona przez inną przeglądarkę lub natywną aplikację.",
-  "error.unexpected_crash.next_steps_addons": "Spróbuj je wyłączyć lub odświeżyć stronę. Jeśli to nie pomoże, możesz wciąż korzystać z Mastodona w innej przeglądarce lub natywnej aplikacji.",
+  "empty_column.explore_statuses": "Nic nie cieszy się teraz popularnością. Sprawdź później!",
+  "empty_column.favourited_statuses": "Nie polubiono jeszcze żadnego wpisu. Gdy to zrobisz, pojawi się tutaj.",
+  "empty_column.favourites": "Nikt jeszcze nie polubił tego wpisu. Kiedy ktoś to zrobi, pojawi się tutaj.",
+  "empty_column.follow_requests": "Nie masz jeszcze żadnych próśb o obserwowanie. Gdy je otrzymasz, pojawią się tutaj.",
+  "empty_column.followed_tags": "Nie obserwujesz jeszcze żadnych hashtagów. Gdy to zrobisz, pojawią się tutaj.",
+  "empty_column.hashtag": "Nie ma jeszcze wpisów oznaczonych tym hasztagiem.",
+  "empty_column.home": "Twoja główna oś czasu jest pusta! Zaobserwuj więcej osób, aby coś zobaczyć.",
+  "empty_column.list": "Nie ma jeszcze nic na tej liście. Kiedy osoby z tej listy opublikują nowe wpisy, pojawią się tutaj.",
+  "empty_column.mutes": "Nie wyciszono jeszcze żadnego użytkownika.",
+  "empty_column.notification_requests": "Wszystko przeczytane! Gdy otrzymasz nowe powiadomienia, pojawią się tutaj zgodnie z twoimi ustawieniami.",
+  "empty_column.notifications": "Nie masz jeszcze żadnych powiadomień. Gdy inne osoby wejdą z tobą w interakcję, zobaczysz to tutaj.",
+  "empty_column.public": "Nic tu nie ma! Opublikuj coś lub obserwuj osoby z innych serwerów, aby coś zobaczyć",
+  "error.unexpected_crash.explanation": "Z powodu błędu w naszym kodzie lub niezgodności przeglądarki nie udało się poprawnie wyświetlić tej strony.",
+  "error.unexpected_crash.explanation_addons": "Nie udało się poprawnie wyświetlić tej strony. Ten błąd jest spowodowany zapewne przez wtyczkę do przeglądarki lub narzędzia do automatycznego tłumaczenia.",
+  "error.unexpected_crash.next_steps": "Spróbuj odświeżyć stronę. Jeśli to nie pomoże, nadal możesz korzystać z Mastodon za pośrednictwem innej przeglądarki lub aplikacji.",
+  "error.unexpected_crash.next_steps_addons": "Spróbuj je wyłączyć i odświeżyć stronę. Jeśli to nie pomoże, nadal możesz korzystać z Mastodon za pośrednictwem innej przeglądarki lub aplikacji.",
   "errors.unexpected_crash.copy_stacktrace": "Skopiuj stacktrace do schowka",
   "errors.unexpected_crash.report_issue": "Zgłoś problem",
   "explore.suggested_follows": "Ludzie",
   "explore.title": "Odkrywaj",
   "explore.trending_links": "Aktualności",
-  "explore.trending_statuses": "Posty",
+  "explore.trending_statuses": "Wpisy",
   "explore.trending_tags": "Hasztagi",
-  "filter_modal.added.context_mismatch_explanation": "Ta kategoria filtrów nie ma zastosowania do kontekstu, w którym uzyskałeś dostęp do tego wpisu. Jeśli chcesz, aby wpis został przefiltrowany również w tym kontekście, będziesz musiał edytować filtr.",
-  "filter_modal.added.context_mismatch_title": "Niezgodność kontekstów!",
-  "filter_modal.added.expired_explanation": "Ta kategoria filtra wygasła, będziesz musiał zmienić datę wygaśnięcia, aby ją zastosować.",
-  "filter_modal.added.expired_title": "Wygasły filtr!",
-  "filter_modal.added.review_and_configure": "Aby przejrzeć i skonfigurować tę kategorię filtrów, przejdź do {settings_link}.",
-  "filter_modal.added.review_and_configure_title": "Ustawienia filtra",
+  "filter_modal.added.context_mismatch_explanation": "To filtrowanie nie dotyczy kategorii, w której pojawił się ten wpis. Jeśli chcesz, aby wpis był filtrowany również w tym kontekście, musisz edytować ustawienia filtrowania.",
+  "filter_modal.added.context_mismatch_title": "Niewłaściwy kontekst!",
+  "filter_modal.added.expired_explanation": "Ta kategoria filtrowania wygasła, aby ją zastosować, należy zmienić datę wygaśnięcia.",
+  "filter_modal.added.expired_title": "Filtr wygasł!",
+  "filter_modal.added.review_and_configure": "Aby przejrzeć i skonfigurować tę kategorię filtrowania, przejdź do {settings_link}.",
+  "filter_modal.added.review_and_configure_title": "Ustawienia filtrowania",
   "filter_modal.added.settings_link": "strona ustawień",
-  "filter_modal.added.short_explanation": "Ten wpis został dodany do następującej kategorii filtrów: {title}.",
-  "filter_modal.added.title": "Filtr dodany!",
-  "filter_modal.select_filter.context_mismatch": "nie dotyczy tego kontekstu",
+  "filter_modal.added.short_explanation": "Ten wpis został dodany do następującej kategorii filtrowania: {title}.",
+  "filter_modal.added.title": "Filtrowanie zostało dodane!",
+  "filter_modal.select_filter.context_mismatch": "nie ma zastosowania w tym kontekście",
   "filter_modal.select_filter.expired": "wygasły",
   "filter_modal.select_filter.prompt_new": "Nowa kategoria: {name}",
   "filter_modal.select_filter.search": "Szukaj lub utwórz",
   "filter_modal.select_filter.subtitle": "Użyj istniejącej kategorii lub utwórz nową",
-  "filter_modal.select_filter.title": "Filtruj ten wpis",
-  "filter_modal.title.status": "Filtruj wpis",
-  "filter_warning.matches_filter": "Pasuje do filtra \"<span>{title}</span>\"",
+  "filter_modal.select_filter.title": "Odfiltruj ten wpis",
+  "filter_modal.title.status": "Odfiltruj wpis",
+  "filter_warning.matches_filter": "Odfiltrowane przez \"<span>{title}</span>\"",
   "filtered_notifications_banner.pending_requests": "Od {count, plural, =0 {żadnej osoby którą możesz znać} one {# osoby którą możesz znać} other {# osób które możesz znać}}",
-  "filtered_notifications_banner.title": "Powiadomienia filtrowane",
+  "filtered_notifications_banner.title": "Odfiltrowane powiadomienia",
   "firehose.all": "Wszystko",
   "firehose.local": "Ten serwer",
   "firehose.remote": "Inne serwery",
-  "follow_request.authorize": "Autoryzuj",
+  "follow_request.authorize": "Przyjmij",
   "follow_request.reject": "Odrzuć",
-  "follow_requests.unlocked_explanation": "Mimo że Twoje konto nie jest zablokowane, zespół {domain} uznał że możesz chcieć ręcznie przejrzeć prośby o możliwość obserwacji.",
-  "follow_suggestions.curated_suggestion": "Wybrane przez personel",
+  "follow_requests.unlocked_explanation": "Mimo że twoje konto nie jest zablokowane, administratorzy {domain} uznali, że możesz chcieć samodzielnie sprawdzić prośby o obserwowanie od tych osób.",
+  "follow_suggestions.curated_suggestion": "Wybrane przez redakcję",
   "follow_suggestions.dismiss": "Nie pokazuj ponownie",
-  "follow_suggestions.featured_longer": "Wybrane przez zespół {domain}",
-  "follow_suggestions.friends_of_friends_longer": "Popularni wśród ludzi których obserwujesz",
-  "follow_suggestions.hints.featured": "Ten profil został wybrany przez zespół {domain}.",
-  "follow_suggestions.hints.friends_of_friends": "Ten profil jest popularny w gronie użytkowników, których obserwujesz.",
+  "follow_suggestions.featured_longer": "Wybrane przez redakcję {domain}",
+  "follow_suggestions.friends_of_friends_longer": "Popularne wśród obserwowanych",
+  "follow_suggestions.hints.featured": "Ten profil został wybrany przez redakcję {domain}.",
+  "follow_suggestions.hints.friends_of_friends": "Ten profil jest popularny wśród obserwowanych.",
   "follow_suggestions.hints.most_followed": "Ten profil jest jednym z najczęściej obserwowanych na {domain}.",
-  "follow_suggestions.hints.most_interactions": "Ten profil otrzymuje dużo interakcji na {domain}.",
-  "follow_suggestions.hints.similar_to_recently_followed": "Ten profil jest podobny do profili ostatnio przez ciebie zaobserwowanych.",
-  "follow_suggestions.personalized_suggestion": "Sugestia spersonalizowana",
-  "follow_suggestions.popular_suggestion": "Sugestia popularna",
-  "follow_suggestions.popular_suggestion_longer": "Popularni na {domain}",
-  "follow_suggestions.similar_to_recently_followed_longer": "Podobne do ostatnio zaobserwowanych przez ciebie profilów",
+  "follow_suggestions.hints.most_interactions": "Ten profil cieszy się ostatnio dużym zainteresowaniem na {domain}.",
+  "follow_suggestions.hints.similar_to_recently_followed": "Ten profil jest podobny do ostatnio przez ciebie zaobserwowanych.",
+  "follow_suggestions.personalized_suggestion": "Spersonalizowana rekomendacja",
+  "follow_suggestions.popular_suggestion": "Popularna rekomendacja",
+  "follow_suggestions.popular_suggestion_longer": "Popularne na {domain}",
+  "follow_suggestions.similar_to_recently_followed_longer": "Podobne do ostatnio zaobserwowanych",
   "follow_suggestions.view_all": "Pokaż wszystkie",
-  "follow_suggestions.who_to_follow": "Kogo obserwować",
+  "follow_suggestions.who_to_follow": "Kogo warto obserwować",
   "followed_tags": "Obserwowane hasztagi",
   "footer.about": "O serwerze",
-  "footer.directory": "Katalog profilów",
+  "footer.directory": "Katalog profili",
   "footer.get_app": "Pobierz aplikację",
   "footer.keyboard_shortcuts": "Skróty klawiszowe",
   "footer.privacy_policy": "Polityka prywatności",
   "footer.source_code": "Zobacz kod źródłowy",
   "footer.status": "Status",
-  "footer.terms_of_service": "Warunki korzystania z usługi",
+  "footer.terms_of_service": "Regulamin",
   "generic.saved": "Zapisano",
-  "getting_started.heading": "Rozpocznij",
-  "hashtag.admin_moderation": "Otwórz interfejs moderacyjny dla #{name}",
+  "getting_started.heading": "Pierwsze kroki",
+  "hashtag.admin_moderation": "Otwórz interfejs moderacji #{name}",
   "hashtag.column_header.tag_mode.all": "i {additional}",
   "hashtag.column_header.tag_mode.any": "lub {additional}",
   "hashtag.column_header.tag_mode.none": "bez {additional}",
-  "hashtag.column_settings.select.no_options_message": "Nie odnaleziono sugestii",
+  "hashtag.column_settings.select.no_options_message": "Nie znaleziono żadnych rekomendacji",
   "hashtag.column_settings.select.placeholder": "Wprowadź hasztagi…",
   "hashtag.column_settings.tag_mode.all": "Wszystkie",
   "hashtag.column_settings.tag_mode.any": "Dowolne",
   "hashtag.column_settings.tag_mode.none": "Żadne",
-  "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
-  "hashtag.counter_by_accounts": "{count, plural, one {{counter} uczestnik} few {{counter} uczestnicy} many {{counter} uczestników} other {{counter} uczestników}}",
+  "hashtag.column_settings.tag_toggle": "Uwzględnij dodatkowe tagi w tej kolumnie",
+  "hashtag.counter_by_accounts": "{count, plural, one {{counter} osoba} few {{counter} osoby} many {{counter} osób} other {{counter} osób}}",
   "hashtag.counter_by_uses": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}",
-  "hashtag.counter_by_uses_today": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}} z dzisiaj",
+  "hashtag.counter_by_uses_today": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}} dzisiaj",
   "hashtag.follow": "Obserwuj hasztag",
   "hashtag.unfollow": "Przestań obserwować hashtag",
   "hashtags.and_other": "…i {count, plural, other {jeszcze #}}",
-  "hints.profiles.followers_may_be_missing": "Może brakować niektórych obserwujących tego profilu.",
-  "hints.profiles.follows_may_be_missing": "Może brakować niektórych obserwowanych przez tego użytkownika.",
-  "hints.profiles.posts_may_be_missing": "Może brakować niektórych wpisów tego profilu.",
-  "hints.profiles.see_more_followers": "Zobacz wszystkich obserwujących na {domain}",
-  "hints.profiles.see_more_follows": "Zobacz wszystkich obserwowanych na {domain}",
-  "hints.profiles.see_more_posts": "Zobacz wszystkie wpisy na {domain}",
-  "hints.threads.replies_may_be_missing": "Może brakować odpowiedzi z innych serwerów.",
-  "hints.threads.see_more": "Zobacz wszystkie odpowiedzi na {domain}",
+  "hints.profiles.followers_may_be_missing": "Niektórzy obserwujący ten profil mogą być niewidoczni.",
+  "hints.profiles.follows_may_be_missing": "Niektórzy obserwowani mogą być niewidoczni.",
+  "hints.profiles.posts_may_be_missing": "Niektóre wpisy mogą być niewidoczne.",
+  "hints.profiles.see_more_followers": "Zobacz więcej obserwujących na {domain}",
+  "hints.profiles.see_more_follows": "Zobacz więcej obserwowanych na {domain}",
+  "hints.profiles.see_more_posts": "Zobacz więcej wpisów na {domain}",
+  "hints.threads.replies_may_be_missing": "Komentarze z innych serwerów mogą być niewidoczne.",
+  "hints.threads.see_more": "Zobacz więcej komentarzy na {domain}",
   "home.column_settings.show_reblogs": "Pokazuj podbicia",
   "home.column_settings.show_replies": "Pokazuj odpowiedzi",
   "home.hide_announcements": "Ukryj ogłoszenia",
-  "home.pending_critical_update.body": "Zaktualizuj serwer jak tylko będzie to możliwe!",
-  "home.pending_critical_update.link": "Pokaż aktualizacje",
+  "home.pending_critical_update.body": "Prosimy o jak najszybszą aktualizację serwera Mastodon!",
+  "home.pending_critical_update.link": "Zobacz aktualizacje",
   "home.pending_critical_update.title": "Dostępna krytyczna aktualizacja bezpieczeństwa!",
   "home.show_announcements": "Pokaż ogłoszenia",
-  "ignore_notifications_modal.disclaimer": "Mastodon nie może poinformować innych użytkowników że ignorujesz ich powiadomienia. Ignorowanie powiadomień nie zapobieże wysyłaniu wpisów per se. ",
-  "ignore_notifications_modal.filter_instead": "Filtruj zamiast tego",
-  "ignore_notifications_modal.filter_to_act_users": "Dalej będziesz mieć możliwość przyjmować, odrzucać, i raportować użytkowników",
-  "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrowanie może ograniczyć pomyłki",
-  "ignore_notifications_modal.filter_to_review_separately": "Możesz osobno przejrzeć powiadomienia odfiltrowane",
+  "ignore_notifications_modal.disclaimer": "Mastodon nie informuje nikogo o zignorowaniu powiadomienia. Ignorowanie powiadomień nie zapobiegnie wysyłaniu samych wiadomości.",
+  "ignore_notifications_modal.filter_instead": "Zamiast tego odfiltruj",
+  "ignore_notifications_modal.filter_to_act_users": "Przyjmowanie, odrzucanie i zgłaszanie innych będzie nadal możliwe",
+  "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrowanie pomaga uniknąć ewentualnych pomyłek",
+  "ignore_notifications_modal.filter_to_review_separately": "Możesz sprawdzić każde odfiltrowane powiadomienie",
   "ignore_notifications_modal.ignore": "Ignoruj powiadomienia",
-  "ignore_notifications_modal.limited_accounts_title": "Ignoruj powiadomienia od kont moderowanych?",
-  "ignore_notifications_modal.new_accounts_title": "Ignoruj powiadomienia od nowych kont?",
-  "ignore_notifications_modal.not_followers_title": "Ignoruj powiadomienia od użytkowników którzy cię nie obserwują?",
-  "ignore_notifications_modal.not_following_title": "Ignoruj powiadomienia od użytkowników których nie obserwujesz?",
-  "ignore_notifications_modal.private_mentions_title": "Ignoruj powiadomienia o nieproszonych wzmiankach prywatnych?",
-  "interaction_modal.action.favourite": "Aby kontynuować, musisz dodać do ulubionych na swoim koncie.",
+  "ignore_notifications_modal.limited_accounts_title": "Ignorować powiadomienia z moderowanych kont?",
+  "ignore_notifications_modal.new_accounts_title": "Ignorować powiadomienia z nowych kont?",
+  "ignore_notifications_modal.not_followers_title": "Ignorować powiadomienia od osób, które cię nie obserwują?",
+  "ignore_notifications_modal.not_following_title": "Ignorować powiadomienia od osób, których nie obserwujesz?",
+  "ignore_notifications_modal.private_mentions_title": "Ignorować powiadomienia od niechcianych wzmianek bezpośrednich?",
+  "info_button.label": "Pomoc",
+  "info_button.what_is_alt_text": "<h1>Czym jest tekst alternatywny?</h1><p>Tekst alternatywny zawiera opisy zdjęć dla osób niedowidzących, korzystających z połączeń o niskiej przepustowości lub szukających dodatkowego kontekstu.</p>\n<p>Możesz poprawić dostępność i czytelność dla wszystkich, pisząc jasny, zwięzły i precyzyjny tekst alternatywny.</p>\n<ul>\n<li>Podkreśl ważne elementy</li>\n<li>Streść tekst widoczny na zdjęciach</li>\n<li>Używaj poprawnej struktury zdań</li>\n<li>Unikaj zbędnych informacji</li>\n<li>Skoncentruj się na kluczowych informacjach zawartych w złożonych wizualizacjach (takich jak diagramy lub mapy)</li>\n</ul>",
+  "interaction_modal.action.favourite": "Aby kontynuować, musisz polubić ze swojego konta.",
   "interaction_modal.action.follow": "Aby kontynuować, musisz obserwować ze swojego konta.",
-  "interaction_modal.action.reblog": "Aby kontynuować, musisz podać dalej ze swojego konta.",
-  "interaction_modal.action.reply": "Aby kontynuować, musisz odpowiedzieć ze swojego konta.",
+  "interaction_modal.action.reblog": "Aby kontynuować, musisz podbić ze swojego konta.",
+  "interaction_modal.action.reply": "Aby kontynuować, musisz komentować ze swojego konta.",
   "interaction_modal.action.vote": "Aby kontynuować, musisz zagłosować ze swojego konta.",
   "interaction_modal.go": "Dalej",
   "interaction_modal.no_account_yet": "Nie masz jeszcze konta?",
   "interaction_modal.on_another_server": "Na innym serwerze",
   "interaction_modal.on_this_server": "Na tym serwerze",
-  "interaction_modal.title.favourite": "Polub wpis użytkownika {name}",
-  "interaction_modal.title.follow": "Śledź {name}",
+  "interaction_modal.title.favourite": "Polub wpis {name}",
+  "interaction_modal.title.follow": "Obserwuj {name}",
   "interaction_modal.title.reblog": "Podbij wpis {name}",
   "interaction_modal.title.reply": "Odpowiedz na post {name}",
-  "interaction_modal.title.vote": "Weź udział w głosowaniu {name}",
+  "interaction_modal.title.vote": "Głosuj w ankiecie {name}",
   "interaction_modal.username_prompt": "Np. {example}",
   "intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}",
   "intervals.full.hours": "{number, plural, one {# godzina} few {# godziny} many {# godzin} other {# godzin}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}",
-  "keyboard_shortcuts.back": "aby cofnąć się",
-  "keyboard_shortcuts.blocked": "aby przejść do listy zablokowanych użytkowników",
-  "keyboard_shortcuts.boost": "aby podbić wpis",
-  "keyboard_shortcuts.column": "aby przejść do wpisu z jednej z kolumn",
-  "keyboard_shortcuts.compose": "aby przejść do pola tworzenia wpisu",
+  "keyboard_shortcuts.back": "Wstecz",
+  "keyboard_shortcuts.blocked": "Otwórz listę zablokowanych",
+  "keyboard_shortcuts.boost": "Podbij wpis",
+  "keyboard_shortcuts.column": "Aktywuj kolumnę",
+  "keyboard_shortcuts.compose": "Aktywuj pole tekstowe",
   "keyboard_shortcuts.description": "Opis",
-  "keyboard_shortcuts.direct": "aby otworzyć kolumnę z wzmiankami prywatnymi",
-  "keyboard_shortcuts.down": "aby przejść na dół listy",
-  "keyboard_shortcuts.enter": "aby otworzyć wpis",
+  "keyboard_shortcuts.direct": "aby otworzyć kolumnę wzmianek bezpośrednich",
+  "keyboard_shortcuts.down": "Przesuń w dół na liście",
+  "keyboard_shortcuts.enter": "Otwórz wpis",
   "keyboard_shortcuts.favourite": "Polub wpis",
-  "keyboard_shortcuts.favourites": "Otwórz listę ulubionych wpisów",
-  "keyboard_shortcuts.federated": "aby otworzyć oś czasu federacji",
+  "keyboard_shortcuts.favourites": "Otwórz listę polubionych wpisów",
+  "keyboard_shortcuts.federated": "Otwórz globalną oś czasu",
   "keyboard_shortcuts.heading": "Skróty klawiszowe",
-  "keyboard_shortcuts.home": "aby otworzyć stronę główną",
+  "keyboard_shortcuts.home": "Otwórz stronę główną",
   "keyboard_shortcuts.hotkey": "Skrót klawiszowy",
-  "keyboard_shortcuts.legend": "aby wyświetlić tę legendę",
-  "keyboard_shortcuts.local": "aby otworzyć lokalną oś czasu",
-  "keyboard_shortcuts.mention": "aby wspomnieć o autorze",
-  "keyboard_shortcuts.muted": "aby przejść do listy wyciszonych użytkowników",
-  "keyboard_shortcuts.my_profile": "aby otworzyć własny profil",
-  "keyboard_shortcuts.notifications": "aby otworzyć kolumnę powiadomień",
+  "keyboard_shortcuts.legend": "Wyświetl skróty klawiszowe",
+  "keyboard_shortcuts.local": "Otwórz lokalną oś czasu",
+  "keyboard_shortcuts.mention": "Dodaj wzmiankę",
+  "keyboard_shortcuts.muted": "Otwórz listę wyciszonych",
+  "keyboard_shortcuts.my_profile": "Otwórz swój profil",
+  "keyboard_shortcuts.notifications": "Otwórz kolumnę powiadomień",
   "keyboard_shortcuts.open_media": "Otwórz multimedia",
-  "keyboard_shortcuts.pinned": "aby przejść do listy przypiętych wpisów",
-  "keyboard_shortcuts.profile": "aby przejść do profilu autora wpisu",
-  "keyboard_shortcuts.reply": "aby odpowiedzieć",
-  "keyboard_shortcuts.requests": "aby przejść do listy próśb o możliwość obserwacji",
-  "keyboard_shortcuts.search": "aby przejść do pola wyszukiwania",
-  "keyboard_shortcuts.spoilers": "aby pokazać/ukryć pole CW",
-  "keyboard_shortcuts.start": "aby otworzyć kolumnę „Rozpocznij”",
-  "keyboard_shortcuts.toggle_hidden": "aby wyświetlić lub ukryć wpis spod CW",
-  "keyboard_shortcuts.toggle_sensitivity": "Pokaż/ukryj multimedia",
-  "keyboard_shortcuts.toot": "Stwórz nowy post",
-  "keyboard_shortcuts.translate": "Aby przetłumaczyć post",
-  "keyboard_shortcuts.unfocus": "aby opuścić pole wyszukiwania/pisania",
-  "keyboard_shortcuts.up": "aby przejść na górę listy",
+  "keyboard_shortcuts.pinned": "Otwórz listę przypiętych wpisów",
+  "keyboard_shortcuts.profile": "Otwórz profil",
+  "keyboard_shortcuts.reply": "Skomentuj",
+  "keyboard_shortcuts.requests": "Otwórz listę próśb o obserwowanie",
+  "keyboard_shortcuts.search": "Aktywuj pole wyszukiwania",
+  "keyboard_shortcuts.spoilers": "Pokaż lub ukryj ostrzeżenia",
+  "keyboard_shortcuts.start": "Otwórz kolumnę \"Pierwsze kroki\"",
+  "keyboard_shortcuts.toggle_hidden": "Pokaż lub ukryj tekst z ostrzeżeniem",
+  "keyboard_shortcuts.toggle_sensitivity": "Pokaż lub ukryj multimedia",
+  "keyboard_shortcuts.toot": "Stwórz nowy wpis",
+  "keyboard_shortcuts.translate": "aby przetłumaczyć wpis",
+  "keyboard_shortcuts.unfocus": "Opuść pole tekstowe",
+  "keyboard_shortcuts.up": "Przesuń w górę na liście",
   "lightbox.close": "Zamknij",
   "lightbox.next": "Następne",
   "lightbox.previous": "Poprzednie",
-  "lightbox.zoom_in": "Rozmiar rzeczywisty",
-  "lightbox.zoom_out": "Dopasuj",
+  "lightbox.zoom_in": "Powiększ do rzeczywistego rozmiaru",
+  "lightbox.zoom_out": "Powiększ, aby dopasować",
   "limited_account_hint.action": "Pokaż profil mimo to",
   "limited_account_hint.title": "Ten profil został ukryty przez moderatorów {domain}.",
   "link_preview.author": "{name}",
@@ -481,230 +486,230 @@
   "lists.add_to_list": "Dodaj do listy",
   "lists.add_to_lists": "Dodaj {name} do list",
   "lists.create": "Utwórz",
-  "lists.create_a_list_to_organize": "Stwórz nową listę żeby zorganizować swoją oś czasu",
+  "lists.create_a_list_to_organize": "Utwórz nową listę, aby uporządkować swoją oś czasu",
   "lists.create_list": "Utwórz listę",
   "lists.delete": "Usuń listę",
   "lists.done": "Gotowe",
   "lists.edit": "Edytuj listę",
-  "lists.exclusive": "Schowaj członków z osi czasu",
-  "lists.exclusive_hint": "Jeśli ktoś jest na tej liście, ukrywa go z Twojej osi czasu, aby uniknąć pokazywania jego wpisów dwukrotnie.",
-  "lists.find_users_to_add": "Znajdź użytkowników do dodania",
-  "lists.list_members": "Członkowie listy",
-  "lists.list_members_count": "{count, plural, one {# użytkownik} other {# użytkowników}}",
+  "lists.exclusive": "Nie pokazuj w mojej osi czasu",
+  "lists.exclusive_hint": "Wpisy osób znajdujących się na tej liście nie będą wyświetlane w twojej osi czasu, aby uniknąć duplikowania tych samych wpisów.",
+  "lists.find_users_to_add": "Znajdź kogoś, aby dodać",
+  "lists.list_members": "Lista osób",
+  "lists.list_members_count": "{count, plural, one {# osoba} few {# osoby} many {# osób} other {# osób}}",
   "lists.list_name": "Nazwa listy",
   "lists.new_list_name": "Nazwa nowej listy",
-  "lists.no_lists_yet": "Nie masz jeszcze list.",
-  "lists.no_members_yet": "Brak członków.",
-  "lists.no_results_found": "Brak wyników.",
+  "lists.no_lists_yet": "Brak list.",
+  "lists.no_members_yet": "Pusto.",
+  "lists.no_results_found": "Nic nie znaleziono.",
   "lists.remove_member": "Usuń",
-  "lists.replies_policy.followed": "Dowolny obserwowany użytkownik",
-  "lists.replies_policy.list": "Członkowie listy",
+  "lists.replies_policy.followed": "Każdy obserwowany",
+  "lists.replies_policy.list": "Osoby na liście",
   "lists.replies_policy.none": "Nikt",
   "lists.save": "Zapisz",
   "lists.search": "Szukaj",
-  "lists.show_replies_to": "Pokaż odpowiedzi od członków listy do",
-  "load_pending": "{count, plural, one {# nowa pozycja} other {nowe pozycje}}",
-  "loading_indicator.label": "Ładowanie…",
+  "lists.show_replies_to": "Uwzględnij komentarze osób z listy do",
+  "load_pending": "{count, plural, one {# nowa} few {# nowe} many {# nowych} other {# nowych}}",
+  "loading_indicator.label": "Wczytywanie…",
   "media_gallery.hide": "Ukryj",
   "moved_to_account_banner.text": "Twoje konto {disabledAccount} jest obecnie wyłączone, ponieważ zostało przeniesione na {movedToAccount}.",
-  "mute_modal.hide_from_notifications": "Ukryj z powiadomień",
+  "mute_modal.hide_from_notifications": "Nie pokazuj w powiadomieniach",
   "mute_modal.hide_options": "Ukryj opcje",
-  "mute_modal.indefinite": "Do ręcznego usunięcia wyciszenia",
+  "mute_modal.indefinite": "Dopóki nie zmienię zdania",
   "mute_modal.show_options": "Pokaż opcje",
-  "mute_modal.they_can_mention_and_follow": "Użytkownik może Cię obserwować oraz dodawać wzmianki, ale Ty ich nie zobaczysz.",
-  "mute_modal.they_wont_know": "Użytkownik nie dowie się, że został wyciszony.",
-  "mute_modal.title": "Wyciszyć użytkownika?",
-  "mute_modal.you_wont_see_mentions": "Nie zobaczysz wpisów, które wspominają tego użytkownika.",
-  "mute_modal.you_wont_see_posts": "Użytkownik dalej będzie widzieć Twoje posty, ale Ty nie będziesz widzieć jego.",
+  "mute_modal.they_can_mention_and_follow": "Może cię wzmiankować i obserwować, ale ty tego nie zobaczysz.",
+  "mute_modal.they_wont_know": "Informacja o wyciszeniu nie będzie widoczna dla tej osoby.",
+  "mute_modal.title": "Wyciszyć?",
+  "mute_modal.you_wont_see_mentions": "Nie zobaczysz wpisów wzmiankujących tę osobę.",
+  "mute_modal.you_wont_see_posts": "Nie zobaczysz wpisów tej osoby, ale ona może widzieć twoje.",
   "navigation_bar.about": "O serwerze",
   "navigation_bar.administration": "Administracja",
-  "navigation_bar.advanced_interface": "Otwórz w zaawansowanym interfejsie użytkownika",
-  "navigation_bar.blocks": "Zablokowani użytkownicy",
+  "navigation_bar.advanced_interface": "Otwórz w widoku zaawansowanym",
+  "navigation_bar.blocks": "Zablokowani",
   "navigation_bar.bookmarks": "Zakładki",
   "navigation_bar.community_timeline": "Lokalna oś czasu",
   "navigation_bar.compose": "Utwórz nowy wpis",
-  "navigation_bar.direct": "Prywatne wzmianki",
+  "navigation_bar.direct": "Wzmianki bezpośrednie",
   "navigation_bar.discover": "Odkrywaj",
-  "navigation_bar.domain_blocks": "Ukryte domeny",
+  "navigation_bar.domain_blocks": "Zablokowane domeny",
   "navigation_bar.explore": "Odkrywaj",
-  "navigation_bar.favourites": "Ulubione",
+  "navigation_bar.favourites": "Polubione",
   "navigation_bar.filters": "Wyciszone słowa",
-  "navigation_bar.follow_requests": "Prośby o obserwację",
+  "navigation_bar.follow_requests": "Prośby o obserwowanie",
   "navigation_bar.followed_tags": "Obserwowane hasztagi",
   "navigation_bar.follows_and_followers": "Obserwowani i obserwujący",
   "navigation_bar.lists": "Listy",
   "navigation_bar.logout": "Wyloguj",
   "navigation_bar.moderation": "Moderacja",
-  "navigation_bar.mutes": "Wyciszeni użytkownicy",
-  "navigation_bar.opened_in_classic_interface": "Posty, konta i inne konkretne strony są otwierane domyślnie w klasycznym interfejsie sieciowym.",
+  "navigation_bar.mutes": "Wyciszeni",
+  "navigation_bar.opened_in_classic_interface": "Wpisy, konta i inne określone strony są domyślnie otwierane w widoku klasycznym.",
   "navigation_bar.personal": "Osobiste",
   "navigation_bar.pins": "Przypięte wpisy",
-  "navigation_bar.preferences": "Preferencje",
+  "navigation_bar.preferences": "Ustawienia",
   "navigation_bar.public_timeline": "Globalna oś czasu",
   "navigation_bar.search": "Szukaj",
   "navigation_bar.security": "Bezpieczeństwo",
-  "not_signed_in_indicator.not_signed_in": "Musisz się zalogować, aby uzyskać dostęp do tego zasobu.",
+  "not_signed_in_indicator.not_signed_in": "Zaloguj się, aby uzyskać dostęp.",
   "notification.admin.report": "{name} zgłosił {target}",
   "notification.admin.report_account": "{name} zgłosił(a) {count, plural, one {1 wpis} few {# wpisy} other {# wpisów}} z {target} w kategorii {category}",
   "notification.admin.report_account_other": "{name} zgłosił(a) {count, plural, one {1 wpis} few {# wpisy} other {# wpisów}} z {target}",
   "notification.admin.report_statuses": "{name} zgłosił(a) {target} w kategorii {category}",
   "notification.admin.report_statuses_other": "{name} zgłosił(a) {target}",
-  "notification.admin.sign_up": "Użytkownik {name} zarejestrował się",
-  "notification.admin.sign_up.name_and_others": "zarejestrował(-a) się {name} i {count, plural, one {# inna osoba} few {# inne osoby}  other {# innych osób}}",
+  "notification.admin.sign_up": "{name} rejestruje się",
+  "notification.admin.sign_up.name_and_others": "{name} i {count, plural, one {# inna osoba} few {# inne osoby}  other {# innych osób}} zarejestrowali się",
   "notification.annual_report.message": "Twój {year} #Wrapstodon jest już dostępny! Zobacz swoje podsumowanie roku i niezapomniane chwile na Mastodon!",
   "notification.annual_report.view": "Zobacz #Wrapstodon",
-  "notification.favourite": "{name} dodaje Twój wpis do ulubionych",
-  "notification.favourite.name_and_others_with_link": "{name} i <a>{count, plural, one {# inna osoba</a> polubiła twój wpis} few {# inne osoby</a> polubiły twój wpis} other {# innych osób</a> polubiło twój wpis}}",
-  "notification.favourite_pm": "{name} polubił(-a) twoją prywatną wzmiankę",
-  "notification.favourite_pm.name_and_others_with_link": "{name} i <a>{count, plural, one {# inna osoba} few {# inne osoby} other {# innych osób}}</a> polubiło twoją prywatną wzmiankę",
-  "notification.follow": "{name} obserwuje Cię",
-  "notification.follow.name_and_others": "{name} i {count, plural, one {<a># inna osoba</a> cię zaobserwowała} few {<a># inne osoby</a> cię zaobserwowały} other {<a># innych osób</a> cię zaobserwowało}}",
+  "notification.favourite": "{name} lubi twój wpis",
+  "notification.favourite.name_and_others_with_link": "{name} i <a>{count, plural, one {# inna osoba} few {# inne osoby} other {# innych osób}}</a> lubią twój wpis",
+  "notification.favourite_pm": "{name} lubi wzmiankę bezpośrednią od ciebie",
+  "notification.favourite_pm.name_and_others_with_link": "{name} i <a>{count, plural, one {# inna osoba} few {# inne osoby} other {# innych osób}}</a> lubią wzmiankę bezpośrednią od ciebie",
+  "notification.follow": "{name} obserwuje cię",
+  "notification.follow.name_and_others": "{name} i <a>{count, plural, one {# inna osoba} few {# inne osoby} other {# innych osób}}</a> zaobserwowali cię",
   "notification.follow_request": "{name} chce cię zaobserwować",
-  "notification.follow_request.name_and_others": "{name} i {count, plural, one {# inna osoba chce} few {# inne osoby chcą} other {# innych osób chce}} zaobserwować twój profil",
+  "notification.follow_request.name_and_others": "{name} i {count, plural, one {# inna osoba} few {# inne osoby} other {# innych osób}} chcą cię zaobserwować",
   "notification.label.mention": "Wzmianka",
-  "notification.label.private_mention": "Prywatna wzmianka",
-  "notification.label.private_reply": "Odpowiedź prywatna",
-  "notification.label.reply": "Odpowiedź",
+  "notification.label.private_mention": "Wzmianka bezpośrednia",
+  "notification.label.private_reply": "Komentarz bezpośredni",
+  "notification.label.reply": "Komentarz",
   "notification.mention": "Wzmianka",
-  "notification.mentioned_you": "{name} wspomniał(a) o Tobie",
+  "notification.mentioned_you": "{name} wzmiankuje cię",
   "notification.moderation-warning.learn_more": "Dowiedz się więcej",
-  "notification.moderation_warning": "Otrzymałeś/-łaś ostrzeżenie moderacyjne",
-  "notification.moderation_warning.action_delete_statuses": "Niektóre twoje wpisy zostały usunięte.",
+  "notification.moderation_warning": "Otrzymano ostrzeżenie",
+  "notification.moderation_warning.action_delete_statuses": "Usunięto niektóre z twoich wpisów.",
   "notification.moderation_warning.action_disable": "Twoje konto zostało wyłączone.",
-  "notification.moderation_warning.action_mark_statuses_as_sensitive": "Niektóre twoje wpisy zostały oznaczone jako wrażliwe.",
-  "notification.moderation_warning.action_none": "Twoje konto otrzymało ostrzeżenie moderacyjne.",
+  "notification.moderation_warning.action_mark_statuses_as_sensitive": "Niektóre z twoich wpisów zostały oznaczone jako wrażliwe.",
+  "notification.moderation_warning.action_none": "Twoje konto otrzymało ostrzeżenie.",
   "notification.moderation_warning.action_sensitive": "Twoje wpisy będą od teraz oznaczane jako wrażliwe.",
   "notification.moderation_warning.action_silence": "Twoje konto zostało ograniczone.",
   "notification.moderation_warning.action_suspend": "Twoje konto zostało zawieszone.",
-  "notification.own_poll": "Twoje głosowanie zakończyło się",
-  "notification.poll": "Głosowanie, w którym brałeś(-aś) udział, zostało zakończone",
+  "notification.own_poll": "Twoja ankieta została zakończona",
+  "notification.poll": "Zakończyła się ankieta, w której głosowano",
   "notification.reblog": "Twój post został podbity przez {name}",
-  "notification.reblog.name_and_others_with_link": "{name} i <a>{count, plural, one {# inna osoba</a> podbiła twój wpis} few {# inne osoby</a> podbiły twój wpis} other {# innych osób</a> podbiło twój wpis}}",
-  "notification.relationships_severance_event": "Utracone związki z {name}",
-  "notification.relationships_severance_event.account_suspension": "Administrator z {from} zawiesił {target}, więc nie dostaniesz wieści ani nie wejdziesz w interakcje z użytkownikami z tego serwera.",
-  "notification.relationships_severance_event.domain_block": "Administrator z {from} zablokował {target}, w tym {followersCount} z Twoich obserwujących i {followingCount, plural, one {# konto} other {# konta}} które obserwujesz.",
+  "notification.reblog.name_and_others_with_link": "{name} i <a>{count, plural, one {# inna osoba} few {# inne osoby} other {# innych osób}}</a> podbili twój wpis",
+  "notification.relationships_severance_event": "Utracono połączenie z {name}",
+  "notification.relationships_severance_event.account_suspension": "Administrator {from} zawiesił {target}, co oznacza, że nie możesz już otrzymywać aktualności ani wchodzić w interakcje z tą osobą.",
+  "notification.relationships_severance_event.domain_block": "Administrator {from} zablokował {target}, w tym {followersCount} twoich obserwujących i {followingCount, plural, one {# konto} few {# konta} other {# kont}}, które obserwujesz.",
   "notification.relationships_severance_event.learn_more": "Dowiedz się więcej",
-  "notification.relationships_severance_event.user_domain_block": "Zablokowałeś {target}, w tym {followersCount} z Twoich obserwujących i {followingCount, plural, one {# konto} other {# konta}} które obserwujesz.",
+  "notification.relationships_severance_event.user_domain_block": "Zablokowałeś {target}, w tym {followersCount} twoich obserwujących i {followingCount, plural, one {# konto} few {# konta} other {# kont}}, które obserwujesz.",
   "notification.status": "{name} opublikował(a) nowy wpis",
   "notification.update": "{name} edytował(a) post",
   "notification_requests.accept": "Akceptuj",
-  "notification_requests.accept_multiple": "Przyjmij {count, plural, one {# wniosek} few {# wnioski} other {# wniosków}} o powiadomienia…",
-  "notification_requests.confirm_accept_multiple.button": "Przyjmij {count, plural, one {wniosek} other {wnioski}} o powiadomienia",
-  "notification_requests.confirm_accept_multiple.message": "Na pewno przyjąć {count, plural, one {# wniosek o powiadomienie} few {# wnioski o powiadomienia} other {# wniosków o powiadomienia}}?",
-  "notification_requests.confirm_accept_multiple.title": "Przyjąć wnioski o powiadomienia?",
-  "notification_requests.confirm_dismiss_multiple.button": "Odrzuć {count, plural, one {wniosek} other {wnioski}} o powiadomienia",
-  "notification_requests.confirm_dismiss_multiple.message": "Na pewno odrzucić {count, plural, one {# wniosek o powiadomienie} few {# wnioski o powiadomienia} other {# wniosków o powiadomienia}}? Stracisz do {count, plural, one {niego}  other {nich}} łatwy dostęp.",
-  "notification_requests.confirm_dismiss_multiple.title": "Odrzuć żądania powiadomień?",
+  "notification_requests.accept_multiple": "Przyjmij {count, plural, one {# prośbę} few {# prośby} other {# próśb}}...",
+  "notification_requests.confirm_accept_multiple.button": "Przyjmij {count, plural, one {# prośbę} few {# prośby} other {# próśb}}",
+  "notification_requests.confirm_accept_multiple.message": "Zamierzasz przyjąć {count, plural, one {# prośbę} few {# prośby} other {# próśb}}. Czy na pewno chcesz kontynuować?",
+  "notification_requests.confirm_accept_multiple.title": "Przyjąć prośby?",
+  "notification_requests.confirm_dismiss_multiple.button": "Odrzuć {count, plural, one {# prośbę} few {# prośby} other {# próśb}}",
+  "notification_requests.confirm_dismiss_multiple.message": "Zamierzasz odrzucić {count, plural, one {# prośbę} few {# prośby} other {# próśb}}. Stracisz do {count, plural, one {tego}  other {tego}} łatwy dostęp. Czy na pewno chcesz kontynuować?",
+  "notification_requests.confirm_dismiss_multiple.title": "Odrzucić prośbę?",
   "notification_requests.dismiss": "Odrzuć",
-  "notification_requests.dismiss_multiple": "Odrzuć {count, plural, one {# wniosek} few {# wnioski} other {# wniosków}} o powiadomienia…",
+  "notification_requests.dismiss_multiple": "Odrzuć {count, plural, one {# prośbę} few {# prośby} other {# próśb}}...",
   "notification_requests.edit_selection": "Edytuj",
   "notification_requests.exit_selection": "Gotowe",
-  "notification_requests.explainer_for_limited_account": "Powiadomienia od tego konta zostały odfiltrowane bo to konto zostało ograniczone przez moderatora.",
-  "notification_requests.explainer_for_limited_remote_account": "Powiadomienia od tego konta zostały odfiltrowane bo to konto, albo serwer na którym się znajduje, zostało ograniczone przez moderatora.",
-  "notification_requests.maximize": "Zmaksymalizuj",
-  "notification_requests.minimize_banner": "Zminimalizuj baner powiadomień filtrowanych",
+  "notification_requests.explainer_for_limited_account": "Powiadomienia z tego konta zostały odfiltrowane, ponieważ konto zostało ograniczone przez moderatora.",
+  "notification_requests.explainer_for_limited_remote_account": "Powiadomienia z tego konta zostały odfiltrowane, ponieważ konto lub serwer zostały ograniczone przez moderatora.",
+  "notification_requests.maximize": "Maksymalizuj",
+  "notification_requests.minimize_banner": "Minimalizuj odfiltrowane powiadomienia",
   "notification_requests.notifications_from": "Powiadomienia od {name}",
-  "notification_requests.title": "Powiadomienia filtrowane",
+  "notification_requests.title": "Odfiltrowane powiadomienia",
   "notification_requests.view": "Wyświetl powiadomienia",
   "notifications.clear": "Wyczyść powiadomienia",
-  "notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?",
+  "notifications.clear_confirmation": "Czy na pewno chcesz trwale wyczyścić wszystkie powiadomienia?",
   "notifications.clear_title": "Wyczyścić powiadomienia?",
   "notifications.column_settings.admin.report": "Nowe zgłoszenia:",
-  "notifications.column_settings.admin.sign_up": "Nowe rejestracje:",
+  "notifications.column_settings.admin.sign_up": "Nowo zarejestrowani:",
   "notifications.column_settings.alert": "Powiadomienia na pulpicie",
-  "notifications.column_settings.favourite": "Ulubione:",
+  "notifications.column_settings.favourite": "Polubione:",
   "notifications.column_settings.filter_bar.advanced": "Wyświetl wszystkie kategorie",
   "notifications.column_settings.filter_bar.category": "Szybkie filtrowanie",
   "notifications.column_settings.follow": "Nowi obserwujący:",
-  "notifications.column_settings.follow_request": "Nowe prośby o możliwość obserwacji:",
+  "notifications.column_settings.follow_request": "Nowe prośby o obserwowanie:",
   "notifications.column_settings.group": "Grupuj",
-  "notifications.column_settings.mention": "Wspomnienia:",
-  "notifications.column_settings.poll": "Wyniki głosowania:",
+  "notifications.column_settings.mention": "Wzmianki:",
+  "notifications.column_settings.poll": "Wyniki ankiety:",
   "notifications.column_settings.push": "Powiadomienia push",
   "notifications.column_settings.reblog": "Podbicia:",
   "notifications.column_settings.show": "Pokaż w kolumnie",
   "notifications.column_settings.sound": "Odtwarzaj dźwięk",
   "notifications.column_settings.status": "Nowe wpisy:",
   "notifications.column_settings.unread_notifications.category": "Nieprzeczytane powiadomienia",
-  "notifications.column_settings.unread_notifications.highlight": "Podświetl nieprzeczytane powiadomienia",
+  "notifications.column_settings.unread_notifications.highlight": "Wyróżnij nieprzeczytane powiadomienia",
   "notifications.column_settings.update": "Edycje:",
   "notifications.filter.all": "Wszystkie",
   "notifications.filter.boosts": "Podbicia",
-  "notifications.filter.favourites": "Ulubione",
+  "notifications.filter.favourites": "Polubione",
   "notifications.filter.follows": "Obserwacje",
-  "notifications.filter.mentions": "Wspomnienia",
-  "notifications.filter.polls": "Wyniki głosowania",
-  "notifications.filter.statuses": "Aktualizacje od osób które obserwujesz",
+  "notifications.filter.mentions": "Wzmianki",
+  "notifications.filter.polls": "Wyniki ankiety",
+  "notifications.filter.statuses": "Aktualności od obserwowanych",
   "notifications.grant_permission": "Przyznaj uprawnienia.",
   "notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}",
   "notifications.mark_as_read": "Oznacz wszystkie powiadomienia jako przeczytane",
-  "notifications.permission_denied": "Powiadomienia na pulpicie nie są dostępne, ponieważ wcześniej nie udzielono uprawnień w przeglądarce",
-  "notifications.permission_denied_alert": "Powiadomienia na pulpicie nie mogą zostać włączone, ponieważ wcześniej odmówiono uprawnień",
-  "notifications.permission_required": "Powiadomienia na pulpicie nie są dostępne, ponieważ nie przyznano wymaganego uprawnienia.",
-  "notifications.policy.accept": "Zaakceptuj",
-  "notifications.policy.accept_hint": "Wyświetlaj w powiadomieniach",
-  "notifications.policy.drop": "Zignoruj",
-  "notifications.policy.drop_hint": "Usuń nieodzyskiwalnie.",
+  "notifications.permission_denied": "Powiadomienia na pulpicie są niedostępne z powodu wcześniejszego braku zgody",
+  "notifications.permission_denied_alert": "Nie można włączyć powiadomień na pulpicie, ponieważ wcześniej nie udzielono zgody",
+  "notifications.permission_required": "Powiadomienia na pulpicie są niedostępne, ponieważ nie przyznano wymaganych uprawnień.",
+  "notifications.policy.accept": "Akceptuj",
+  "notifications.policy.accept_hint": "Pokazuj w powiadomieniach",
+  "notifications.policy.drop": "Ignoruj",
+  "notifications.policy.drop_hint": "Usuń trwale",
   "notifications.policy.filter": "Odfiltruj",
-  "notifications.policy.filter_hint": "Wyślij do skrzynki powiadomień odfiltrowanych",
+  "notifications.policy.filter_hint": "Przenieś do odfiltrowanych powiadomień",
   "notifications.policy.filter_limited_accounts_hint": "Ograniczonych przez moderatorów serwera",
   "notifications.policy.filter_limited_accounts_title": "Kont zmoderowanych",
-  "notifications.policy.filter_new_accounts.hint": "Utworzone w ciągu {days, plural, one {ostatniego dnia} other {ostatnich # dni}}",
-  "notifications.policy.filter_new_accounts_title": "Nowe konta",
-  "notifications.policy.filter_not_followers_hint": "Zawierające osoby które obserwują cię krócej niż {days, plural, one {dzień} other {# dni}}",
-  "notifications.policy.filter_not_followers_title": "Ludzie, którzy cię nie obserwują",
-  "notifications.policy.filter_not_following_hint": "Aż ich ręcznie nie zatwierdzisz",
-  "notifications.policy.filter_not_following_title": "Ludzie, których nie obserwujesz",
-  "notifications.policy.filter_private_mentions_hint": "Odfiltrowane, chyba że są odpowiedzią na twoją własną wzmiankę, lub obserwujesz wysyłającego",
-  "notifications.policy.filter_private_mentions_title": "Nieproszone prywatne wzmianki",
+  "notifications.policy.filter_new_accounts.hint": "Utworzonych w ciągu {days, plural, one {ostatniego dnia} other {ostatnich # dni}}",
+  "notifications.policy.filter_new_accounts_title": "Nowych kont",
+  "notifications.policy.filter_not_followers_hint": "Uwzględniając osoby, które obserwują cię krócej niż {days, plural, one {# dzień} other {# dni}}",
+  "notifications.policy.filter_not_followers_title": "Osób, które cię nie obserwują",
+  "notifications.policy.filter_not_following_hint": "Do momentu zatwierdzenia",
+  "notifications.policy.filter_not_following_title": "Osób, których nie obserwujesz",
+  "notifications.policy.filter_private_mentions_hint": "Odfiltrowane, chyba że są odpowiedzią na wzmiankę od ciebie lub obserwujesz nadawcę",
+  "notifications.policy.filter_private_mentions_title": "Niechcianych wzmianek bezpośrednich",
   "notifications.policy.title": "Zarządzaj powiadomieniami od…",
   "notifications_permission_banner.enable": "Włącz powiadomienia na pulpicie",
-  "notifications_permission_banner.how_to_control": "Aby otrzymywać powiadomienia, gdy Mastodon nie jest otwarty, włącz powiadomienia pulpitu. Możesz dokładnie kontrolować, októrych działaniach będziesz powiadomienia na pulpicie za pomocą przycisku {icon} powyżej, jeżeli tylko zostaną włączone.",
-  "notifications_permission_banner.title": "Nie przegap niczego",
+  "notifications_permission_banner.how_to_control": "Aby otrzymywać powiadomienia, gdy Mastodon nie jest otwarty, włącz powiadomienia na pulpicie. Możesz wybrać, które dokładnie typy interakcji generują powiadomienia na pulpicie za pomocą przycisku {icon} powyżej po ich włączeniu.",
+  "notifications_permission_banner.title": "Nigdy niczego nie przegapisz",
   "onboarding.follows.back": "Wróć",
   "onboarding.follows.done": "Gotowe",
-  "onboarding.follows.empty": "Niestety w tej chwili nie można przedstawić żadnych wyników. Możesz spróbować wyszukać lub przeglądać stronę, aby znaleźć osoby do śledzenia, lub spróbować ponownie później.",
+  "onboarding.follows.empty": "Niestety, w tej chwili nie można nic wyświetlić. Możesz użyć wyszukiwania lub przeglądać stronę główną, aby znaleźć osoby, które chcesz obserwować, albo spróbuj ponownie później.",
   "onboarding.follows.search": "Szukaj",
-  "onboarding.follows.title": "Zaobserwuj konta by zacząć",
-  "onboarding.profile.discoverable": "Spraw mój profil odkrywalnym",
-  "onboarding.profile.discoverable_hint": "Kiedy zapisujesz się do odkrywalności w Mastodonie, twoje wpisy mogą pokazywać się w wynikach wyszukiwania i trendach, a twój profil może być sugerowany użytkownikom o podobnych zainteresowaniach.",
-  "onboarding.profile.display_name": "Nazwa wyświetlana",
+  "onboarding.follows.title": "Zaobserwuj kogoś, aby zacząć",
+  "onboarding.profile.discoverable": "Spraw, by mój profil był widoczny",
+  "onboarding.profile.discoverable_hint": "Gdy zdecydujesz się na włączenie widoczności na Mastodon, twoje wpisy mogą pojawiać się w wynikach wyszukiwania i aktualnościach, a twój profil może być polecany osobom o podobnych zainteresowaniach.",
+  "onboarding.profile.display_name": "Wyświetlana nazwa",
   "onboarding.profile.display_name_hint": "Twoje imię lub pseudonim…",
-  "onboarding.profile.note": "O mnie",
-  "onboarding.profile.note_hint": "Możesz @wspomnieć użytkowników albo #hasztagi…",
+  "onboarding.profile.note": "Opis",
+  "onboarding.profile.note_hint": "Możesz @wzmiankować innych lub dodawać #hashtagi…",
   "onboarding.profile.save_and_continue": "Zapisz i kontynuuj",
   "onboarding.profile.title": "Ustawienia profilu",
   "onboarding.profile.upload_avatar": "Dodaj zdjęcie profilowe",
-  "onboarding.profile.upload_header": "Dodaj banner profilu",
-  "password_confirmation.exceeds_maxlength": "Potwierdzenie hasła przekracza maksymalną długość hasła",
-  "password_confirmation.mismatching": "Wprowadzone hasła różnią się od siebie",
-  "picture_in_picture.restore": "Odłóż",
+  "onboarding.profile.upload_header": "Dodaj baner",
+  "password_confirmation.exceeds_maxlength": "Długość potwierdzonego hasła przekracza maksymalną długość hasła",
+  "password_confirmation.mismatching": "Hasła nie są takie same",
+  "picture_in_picture.restore": "Powrót",
   "poll.closed": "Zamknięte",
   "poll.refresh": "Odśwież",
-  "poll.reveal": "Wyświetl wyniki",
+  "poll.reveal": "Zobacz wyniki",
   "poll.total_people": "{count, plural, one {# osoba} few {# osoby} many {# osób} other {# osób}}",
   "poll.total_votes": "{count, plural, one {# głos} few {# głosy} many {# głosów} other {# głosów}}",
-  "poll.vote": "Zagłosuj",
-  "poll.voted": "Zagłosowałeś_aś na tą odpowiedź",
+  "poll.vote": "Głosuj",
+  "poll.voted": "Wybrano tę odpowiedź",
   "poll.votes": "{votes, plural, one {# głos} few {# głosy} many {# głosów} other {# głosów}}",
-  "poll_button.add_poll": "Dodaj głosowanie",
-  "poll_button.remove_poll": "Usuń głosowanie",
+  "poll_button.add_poll": "Dodaj ankietę",
+  "poll_button.remove_poll": "Usuń ankietę",
   "privacy.change": "Dostosuj widoczność wpisów",
-  "privacy.direct.long": "Wszyscy wspomnieni w tym wpisie",
-  "privacy.direct.short": "Konkretni ludzie",
-  "privacy.private.long": "Tylko ci, którzy cię obserwują",
+  "privacy.direct.long": "Wszyscy wzmiankowani w tym wpisie",
+  "privacy.direct.short": "Wzmianka bezpośrednia",
+  "privacy.private.long": "Tylko obserwujący",
   "privacy.private.short": "Obserwujący",
-  "privacy.public.long": "Ktokolwiek na i poza Mastodonem",
+  "privacy.public.long": "Każdy na i poza Mastodon",
   "privacy.public.short": "Publiczny",
-  "privacy.unlisted.additional": "Taki sam jak \"Publiczny\", ale wpis nie pojawi się w kanałach na żywo, hasztagach, odkrywaniu, ani w wyszukiwaniu w Mastodonie, nawet jeżeli jest to włączone w ustawieniach konta.",
-  "privacy.unlisted.long": "Widoczne dla każdego, z wyłączeniem funkcji odkrywania",
+  "privacy.unlisted.additional": "Dostępny podobnie jak wpis publiczny, ale nie będzie widoczny w aktualnościach, hashtagach ani wyszukiwarce Mastodon, nawet jeśli twoje konto jest widoczne.",
+  "privacy.unlisted.long": "Niewidoczny w aktualnościach",
   "privacy.unlisted.short": "Niewidoczny",
   "privacy_policy.last_updated": "Data ostatniej aktualizacji: {date}",
   "privacy_policy.title": "Polityka prywatności",
   "recommended": "Zalecane",
   "refresh": "Odśwież",
   "regeneration_indicator.please_stand_by": "Proszę czekać.",
-  "regeneration_indicator.preparing_your_home_feed": "Przygotowywanie Twojego kanału wiadomości...",
+  "regeneration_indicator.preparing_your_home_feed": "Wczytywanie twojej osi czasu…",
   "relative_time.days": "{number} dni",
   "relative_time.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}} temu",
   "relative_time.full.hours": "{number, plural, one {# godzinę} few {# godziny} many {# godzin} other {# godzin}} temu",
@@ -716,27 +721,27 @@
   "relative_time.minutes": "{number} min.",
   "relative_time.seconds": "{number} s.",
   "relative_time.today": "dzisiaj",
-  "reply_indicator.attachments": "{count, plural, one {# załącznik} few {# załączniki} many {# załączników} other {# załączniku}}",
+  "reply_indicator.attachments": "{count, plural, one {# załącznik} few {# załączniki} many {# załączników} other {# załączników}}",
   "reply_indicator.cancel": "Anuluj",
   "reply_indicator.poll": "Ankieta",
   "report.block": "Zablokuj",
-  "report.block_explanation": "Nie zobaczysz ich wpisów. Nie będą mogli zobaczyć Twoich postów ani cię obserwować. Będą mogli domyślić się, że są zablokowani.",
+  "report.block_explanation": "Nie zobaczysz wpisów tej osoby, a ona twoich, ani nie będzie mogła cię zaobserwować. Informacja o zablokowaniu będzie widoczna.",
   "report.categories.legal": "Prawne",
   "report.categories.other": "Inne",
   "report.categories.spam": "Spam",
   "report.categories.violation": "Zawartość narusza co najmniej jedną zasadę serwera",
   "report.category.subtitle": "Wybierz najbardziej pasującą opcję",
   "report.category.title": "Powiedz, co się dzieje z tym {type}",
-  "report.category.title_account": "profil",
-  "report.category.title_status": "post",
+  "report.category.title_account": "profilem",
+  "report.category.title_status": "wpisem",
   "report.close": "Gotowe",
-  "report.comment.title": "Czy jest jeszcze coś, co uważasz, że powinniśmy wiedzieć?",
-  "report.forward": "Przekaż na {target}",
-  "report.forward_hint": "To konto znajduje się na innej instancji. Czy chcesz wysłać anonimową kopię zgłoszenia rnież na nią?",
+  "report.comment.title": "Czy jest coś jeszcze, co powinniśmy wiedzieć?",
+  "report.forward": "Prześlij do {target}",
+  "report.forward_hint": "Konto pochodzi z innego serwera. Czy chcesz również tam wysłać kopię zgłoszenia anonimowo?",
   "report.mute": "Wycisz",
-  "report.mute_explanation": "Nie zobaczysz ich wpisów. Mimo to będą mogli wciąż obserwować cię i widzieć twoje wpisy, ale nie będą widzieli, że są wyciszeni.",
+  "report.mute_explanation": "Nie zobaczysz wpisów tej osoby, ale ona nadal będzie mogła cię obserwować i zobaczyć twoje wpisy. Informacja o wyciszeniu nie będzie widoczna.",
   "report.next": "Dalej",
-  "report.placeholder": "Dodatkowe komentarze",
+  "report.placeholder": "Dodatkowe informacje",
   "report.reasons.dislike": "Nie podoba mi się to",
   "report.reasons.dislike_description": "Nie jest to coś, co chciałoby się zobaczyć",
   "report.reasons.legal": "To jest nielegalne",
@@ -897,8 +902,6 @@
   "video.expand": "Rozszerz film",
   "video.fullscreen": "Pełny ekran",
   "video.hide": "Ukryj film",
-  "video.mute": "Wycisz",
   "video.pause": "Pauzuj",
-  "video.play": "Odtwórz",
-  "video.unmute": "Cofnij wyciszenie"
+  "video.play": "Odtwórz"
 }
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 021eeb349b..55cfeea582 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -29,7 +29,6 @@
   "account.endorse": "Recomendar",
   "account.featured_tags.last_status_at": "Última publicação em {date}",
   "account.featured_tags.last_status_never": "Sem publicações",
-  "account.featured_tags.title": "Hashtags em destaque de {name}",
   "account.follow": "Seguir",
   "account.follow_back": "Seguir de volta",
   "account.followers": "Seguidores",
@@ -65,6 +64,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear domínio {domain}",
+  "account.unblock_domain_short": "Desbloquear",
   "account.unblock_short": "Desbloquear",
   "account.unendorse": "Remover",
   "account.unfollow": "Deixar de seguir",
@@ -86,6 +86,13 @@
   "alert.unexpected.message": "Ocorreu um erro inesperado.",
   "alert.unexpected.title": "Eita!",
   "alt_text_badge.title": "Texto alternativo",
+  "alt_text_modal.add_alt_text": "Adicione texto alternativo",
+  "alt_text_modal.add_text_from_image": "Adicione texto da imagem",
+  "alt_text_modal.cancel": "Cancelar",
+  "alt_text_modal.change_thumbnail": "Alterar miniatura",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "…Descreva isso para pessoas com deficiências auditivas.…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Descreva isso para pessoas com deficiências visuais…",
+  "alt_text_modal.done": "Feito",
   "announcement.announcement": "Comunicados",
   "annual_report.summary.archetype.booster": "Caçador legal",
   "annual_report.summary.archetype.lurker": "O espreitador",
@@ -211,6 +218,10 @@
   "confirmations.logout.confirm": "Sair",
   "confirmations.logout.message": "Você tem certeza de que deseja sair?",
   "confirmations.logout.title": "Sair da sessão?",
+  "confirmations.missing_alt_text.confirm": "Adicione texto alternativo",
+  "confirmations.missing_alt_text.message": "Seu post contém mídia sem texto alternativo. Adicionar descrições ajuda a tornar seu conteúdo acessível para mais pessoas.",
+  "confirmations.missing_alt_text.secondary": "Postar mesmo assim",
+  "confirmations.missing_alt_text.title": "Adicionar texto alternativo?",
   "confirmations.mute.confirm": "Silenciar",
   "confirmations.redraft.confirm": "Excluir e rascunhar",
   "confirmations.redraft.message": "Você tem certeza de que quer apagar essa postagem e rascunhá-la? Favoritos e impulsos serão perdidos, e respostas à postagem original ficarão órfãs.",
@@ -407,6 +418,8 @@
   "ignore_notifications_modal.not_followers_title": "Ignorar notificações de pessoas que não seguem você?",
   "ignore_notifications_modal.not_following_title": "Ignorar notificações de pessoas que você não segue?",
   "ignore_notifications_modal.private_mentions_title": "Ignorar notificações de menções privadas não solicitadas?",
+  "info_button.label": "Ajuda",
+  "info_button.what_is_alt_text": "<h1>O que é texto alternativo?</h1><p>O texto alternativo fornece descrições de imagens para pessoas com deficiências visuais, conexões de internet de baixa largura de banda ou aquelas que buscam mais contexto.</p><p>Você pode melhorar a acessibilidade e a compreensão para todos escrevendo texto alternativo claro, conciso e objetivo.</p> <ul> <li>Capture elementos importantes</li> <li>Resuma textos em imagens</li> <li>Use estrutura de frases regular</li> <li>Evite informações redundantes</li> <li>Foque em tendências e descobertas principais em visuais complexos (como diagramas ou mapas)</li> </ul>",
   "interaction_modal.action.favourite": "Para continuar, você precisa favoritar na sua conta.",
   "interaction_modal.action.follow": "Para continuar, você precisa seguir da sua conta.",
   "interaction_modal.action.reblog": "Para continuar, você precisa impulsionar na sua conta.",
@@ -457,6 +470,7 @@
   "keyboard_shortcuts.toggle_hidden": "expandir/ocultar aviso de conteúdo",
   "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar mídia",
   "keyboard_shortcuts.toot": "compor novo toot",
+  "keyboard_shortcuts.translate": "Para traduzir um post",
   "keyboard_shortcuts.unfocus": "desfocar de tudo",
   "keyboard_shortcuts.up": "mover para cima",
   "lightbox.close": "Fechar",
@@ -683,7 +697,7 @@
   "poll_button.remove_poll": "Remover enquete",
   "privacy.change": "Alterar privacidade do toot",
   "privacy.direct.long": "Todos mencionados na publicação",
-  "privacy.direct.short": "Pessoas específicas",
+  "privacy.direct.short": "Menção privada",
   "privacy.private.long": "Apenas seus seguidores",
   "privacy.private.short": "Seguidores",
   "privacy.public.long": "Qualquer um dentro ou fora do Mastodon",
@@ -836,6 +850,7 @@
   "status.reblogs.empty": "Nada aqui. Quando alguém der boost, o usuário aparecerá aqui.",
   "status.redraft": "Excluir e rascunhar",
   "status.remove_bookmark": "Remover do Salvos",
+  "status.remove_favourite": "Remover dos favoritos",
   "status.replied_in_thread": "Respondido na conversa",
   "status.replied_to": "Em resposta a {name}",
   "status.reply": "Responder",
@@ -857,7 +872,9 @@
   "subscribed_languages.target": "Alterar idiomas inscritos para {target}",
   "tabs_bar.home": "Página inicial",
   "tabs_bar.notifications": "Notificações",
+  "terms_of_service.effective_as_of": "Em vigor a partir de {date}",
   "terms_of_service.title": "Termos de serviço",
+  "terms_of_service.upcoming_changes_on": "Próximas mudanças em {date}",
   "time_remaining.days": "{number, plural, one {# dia restante} other {# dias restantes}}",
   "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
   "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",
@@ -888,8 +905,12 @@
   "video.expand": "Abrir vídeo",
   "video.fullscreen": "Tela cheia",
   "video.hide": "Ocultar mídia",
-  "video.mute": "Sem som",
+  "video.mute": "Silenciar",
   "video.pause": "Pausar",
   "video.play": "Executar",
-  "video.unmute": "Com som"
+  "video.skip_backward": "Retroceder",
+  "video.skip_forward": "Avançar",
+  "video.unmute": "Ativar som",
+  "video.volume_down": "Diminuir o volume",
+  "video.volume_up": "Aumentar o volume"
 }
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 801c84f23c..87c9e5846a 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -29,7 +29,6 @@
   "account.endorse": "Destacar no perfil",
   "account.featured_tags.last_status_at": "Última publicação em {date}",
   "account.featured_tags.last_status_never": "Sem publicações",
-  "account.featured_tags.title": "Etiquetas destacadas por {name}",
   "account.follow": "Seguir",
   "account.follow_back": "Seguir também",
   "account.followers": "Seguidores",
@@ -42,7 +41,7 @@
   "account.hide_reblogs": "Esconder partilhas impulsionadas de @{name}",
   "account.in_memoriam": "Em Memória.",
   "account.joined_short": "Juntou-se a",
-  "account.languages": "Alterar línguas subscritas",
+  "account.languages": "Alterar idiomas subscritos",
   "account.link_verified_on": "O proprietário desta hiperligação foi verificado em {date}",
   "account.locked_info": "Esta conta é privada. O proprietário revê manualmente quem o pode seguir.",
   "account.media": "Multimédia",
@@ -65,6 +64,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear o domínio {domain}",
+  "account.unblock_domain_short": "Desbloquear",
   "account.unblock_short": "Desbloquear",
   "account.unendorse": "Não destacar no perfil",
   "account.unfollow": "Deixar de seguir",
@@ -219,7 +219,7 @@
   "confirmations.logout.message": "Tens a certeza de que queres terminar a sessão?",
   "confirmations.logout.title": "Terminar sessão?",
   "confirmations.missing_alt_text.confirm": "Adicionar texto alternativo",
-  "confirmations.missing_alt_text.message": "A sua publicação contém elementos gráficos sem texto alternativo. Adicionar descrições ajuda a tornar o seu conteúdo acessível a mais pessoas.",
+  "confirmations.missing_alt_text.message": "A tua publicação contém multimédia sem texto alternativo. A adição de descrições ajuda a tornar o conteúdo acessível a mais pessoas.",
   "confirmations.missing_alt_text.secondary": "Publicar mesmo assim",
   "confirmations.missing_alt_text.title": "Adicionar texto alternativo?",
   "confirmations.mute.confirm": "Ocultar",
@@ -697,7 +697,7 @@
   "poll_button.remove_poll": "Remover sondagem",
   "privacy.change": "Alterar a privacidade da publicação",
   "privacy.direct.long": "Todos os mencionados na publicação",
-  "privacy.direct.short": "Pessoas específicas",
+  "privacy.direct.short": "Menção privada",
   "privacy.private.long": "Apenas os teus seguidores",
   "privacy.private.short": "Seguidores",
   "privacy.public.long": "Qualquer pessoa no Mastodon ou não",
@@ -817,7 +817,7 @@
   "status.cancel_reblog_private": "Retirar impulso",
   "status.cannot_reblog": "Esta publicação não pode ser impulsionada",
   "status.continued_thread": "Continuação da conversa",
-  "status.copy": "Copiar hiperligação para a publicação",
+  "status.copy": "Copiar hiperligação da publicação",
   "status.delete": "Eliminar",
   "status.detailed_status": "Vista pormenorizada da conversa",
   "status.direct": "Mencionar @{name} em privado",
@@ -872,7 +872,9 @@
   "subscribed_languages.target": "Alterar idiomas subscritos para {target}",
   "tabs_bar.home": "Início",
   "tabs_bar.notifications": "Notificações",
+  "terms_of_service.effective_as_of": "Válido a partir de {date}",
   "terms_of_service.title": "Termos do serviço",
+  "terms_of_service.upcoming_changes_on": "Próximas aterações em {date}",
   "time_remaining.days": "{number, plural, one {# dia restante} other {# dias restantes}}",
   "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
   "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",
@@ -903,8 +905,6 @@
   "video.expand": "Expandir vídeo",
   "video.fullscreen": "Ecrã completo",
   "video.hide": "Ocultar vídeo",
-  "video.mute": "Desativar som",
   "video.pause": "Pausar",
-  "video.play": "Reproduzir",
-  "video.unmute": "Ativar som"
+  "video.play": "Reproduzir"
 }
diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json
index bf002011df..8fec42bbd0 100644
--- a/app/javascript/mastodon/locales/ro.json
+++ b/app/javascript/mastodon/locales/ro.json
@@ -29,7 +29,6 @@
   "account.endorse": "Promovează pe profil",
   "account.featured_tags.last_status_at": "Ultima postare pe {date}",
   "account.featured_tags.last_status_never": "Fără postări",
-  "account.featured_tags.title": "Haștagurile recomandate de {name}",
   "account.follow": "Urmărește",
   "account.follow_back": "Urmăreşte înapoi",
   "account.followers": "Urmăritori",
@@ -597,8 +596,6 @@
   "video.expand": "Extinde video",
   "video.fullscreen": "Ecran complet",
   "video.hide": "Ascunde video",
-  "video.mute": "Oprește sonorul",
   "video.pause": "Pauză",
-  "video.play": "Redare",
-  "video.unmute": "Repornește sunetul"
+  "video.play": "Redare"
 }
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 116b28e4d4..aaa73f51be 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -3,7 +3,7 @@
   "about.contact": "Связаться:",
   "about.disclaimer": "Mastodon — свободное программное обеспечение с открытым исходным кодом и торговая марка Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Причина не указана",
-  "about.domain_blocks.preamble": "Mastodon обычно позволяет просматривать содержимое и взаимодействовать с другими пользователями любых серверов в Федиверсе. Вот исключения, сделанные конкретно для этого сервера.",
+  "about.domain_blocks.preamble": "Mastodon обычно позволяет просматривать содержимое и взаимодействовать с пользователями любых других серверов в федивёрсе. Вот исключения, сделанные конкретно для этого сервера.",
   "about.domain_blocks.silenced.explanation": "Как правило, вы не увидите профили и контент с этого сервера, если вы специально не будете их искать или не подпишетесь на них.",
   "about.domain_blocks.silenced.title": "Ограничивается",
   "about.domain_blocks.suspended.explanation": "Никакие данные с этого сервера не будут обрабатываться, храниться или обмениваться, что делает невозможным любое взаимодействие или связь с пользователями с этого сервера.",
@@ -18,18 +18,20 @@
   "account.block": "Заблокировать @{name}",
   "account.block_domain": "Заблокировать {domain}",
   "account.block_short": "Заблокировать",
-  "account.blocked": "Заблокировано",
+  "account.blocked": "Заблокирован(а)",
   "account.cancel_follow_request": "Отозвать запрос на подписку",
-  "account.copy": "Скопировать ссылку на профиль",
+  "account.copy": "Копировать ссылку на профиль",
   "account.direct": "Упомянуть @{name} лично",
   "account.disable_notifications": "Не уведомлять о постах от @{name}",
   "account.domain_blocked": "Домен заблокирован",
-  "account.edit_profile": "Редактировать профиль",
+  "account.edit_profile": "Редактировать",
   "account.enable_notifications": "Уведомлять о постах от @{name}",
   "account.endorse": "Рекомендовать в профиле",
+  "account.featured": "Избранное",
+  "account.featured.hashtags": "Хэштеги",
+  "account.featured.posts": "Посты",
   "account.featured_tags.last_status_at": "Последний пост {date}",
   "account.featured_tags.last_status_never": "Нет постов",
-  "account.featured_tags.title": "Избранные хэштеги {name}",
   "account.follow": "Подписаться",
   "account.follow_back": "Подписаться в ответ",
   "account.followers": "Подписчики",
@@ -40,20 +42,20 @@
   "account.follows.empty": "Этот пользователь пока ни на кого не подписался.",
   "account.go_to_profile": "Перейти к профилю",
   "account.hide_reblogs": "Скрыть продвижения от @{name}",
-  "account.in_memoriam": "Вечная память.",
+  "account.in_memoriam": "In Memoriam.",
   "account.joined_short": "Дата регистрации",
   "account.languages": "Изменить языки подписки",
   "account.link_verified_on": "Владение этой ссылкой было проверено {date}",
-  "account.locked_info": "Это закрытый аккаунт. Его владелец вручную одобряет подписчиков.",
+  "account.locked_info": "Это закрытая учётная запись. Её владелец вручную одобряет подписчиков.",
   "account.media": "Медиа",
   "account.mention": "Упомянуть @{name}",
-  "account.moved_to": "У {name} теперь новый аккаунт:",
+  "account.moved_to": "У {name} теперь новая учётная запись:",
   "account.mute": "Игнорировать @{name}",
   "account.mute_notifications_short": "Отключить уведомления",
-  "account.mute_short": "Приглушить",
+  "account.mute_short": "Игнорировать",
   "account.muted": "Игнорируется",
-  "account.mutual": "Взаимно",
-  "account.no_bio": "Описание не предоставлено.",
+  "account.mutual": "Взаимные подписки",
+  "account.no_bio": "Описание профиля отсутствует.",
   "account.open_original_page": "Открыть исходную страницу",
   "account.posts": "Посты",
   "account.posts_with_replies": "Посты и ответы",
@@ -65,27 +67,35 @@
   "account.statuses_counter": "{count, plural, one {{counter} пост} few {{counter} поста} other {{counter} постов}}",
   "account.unblock": "Разблокировать @{name}",
   "account.unblock_domain": "Разблокировать {domain}",
+  "account.unblock_domain_short": "Разблокировать",
   "account.unblock_short": "Разблокировать",
   "account.unendorse": "Не рекомендовать в профиле",
   "account.unfollow": "Отписаться",
-  "account.unmute": "Перестать игнорировать @{name}",
+  "account.unmute": "Не игнорировать @{name}",
   "account.unmute_notifications_short": "Включить уведомления",
   "account.unmute_short": "Не игнорировать",
   "account_note.placeholder": "Текст заметки",
   "admin.dashboard.daily_retention": "Уровень удержания пользователей после регистрации, в днях",
   "admin.dashboard.monthly_retention": "Уровень удержания пользователей после регистрации, в месяцах",
-  "admin.dashboard.retention.average": "В среднем за всё время",
+  "admin.dashboard.retention.average": "В среднем",
   "admin.dashboard.retention.cohort": "Месяц регистрации",
   "admin.dashboard.retention.cohort_size": "Новые пользователи",
   "admin.impact_report.instance_accounts": "Профили учетных записей, которые будут удалены",
   "admin.impact_report.instance_followers": "Подписчики, которых потеряют наши пользователи",
   "admin.impact_report.instance_follows": "Подписчики, которых потеряют их пользователи",
   "admin.impact_report.title": "Резюме воздействия",
-  "alert.rate_limited.message": "Пожалуйста, повторите после {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Ограничение количества запросов",
+  "alert.rate_limited.message": "Подождите до {retry_time, time, medium}, прежде чем делать что-либо ещё.",
+  "alert.rate_limited.title": "Слишком много запросов",
   "alert.unexpected.message": "Произошла непредвиденная ошибка.",
   "alert.unexpected.title": "Ой!",
   "alt_text_badge.title": "Альтернативный текст",
+  "alt_text_modal.add_alt_text": "Альтернативный текст",
+  "alt_text_modal.add_text_from_image": "Добавить текст из изображения",
+  "alt_text_modal.cancel": "Отмена",
+  "alt_text_modal.change_thumbnail": "Изменить обложку",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Опишите то, что слышите, для людей с нарушениями слуха…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Опишите то, что видите, для людей с нарушениями зрения…",
+  "alt_text_modal.done": "Готово",
   "announcement.announcement": "Объявление",
   "annual_report.summary.archetype.booster": "Репостер",
   "annual_report.summary.archetype.lurker": "Молчун",
@@ -118,23 +128,23 @@
   "block_modal.you_wont_see_mentions": "Вы не увидите посты, которые его упоминают.",
   "boost_modal.combo": "{combo}, чтобы пропустить это в следующий раз",
   "boost_modal.reblog": "Продвинуть пост?",
-  "boost_modal.undo_reblog": "Убрать продвижение?",
-  "bundle_column_error.copy_stacktrace": "Скопировать отчет об ошибке",
+  "boost_modal.undo_reblog": "Отменить продвижение?",
+  "bundle_column_error.copy_stacktrace": "Копировать отчёт об ошибке",
   "bundle_column_error.error.body": "Запрошенная страница не может быть отображена. Это может быть вызвано ошибкой в нашем коде или проблемой совместимости браузера.",
   "bundle_column_error.error.title": "О нет!",
-  "bundle_column_error.network.body": "При загрузке этой страницы произошла ошибка. Это может быть связано с Вашим Интернет-соединением или неполадками на сервере.",
+  "bundle_column_error.network.body": "При загрузке этой страницы произошла ошибка. Это может быть вызвано временными проблемами с вашим подключением к интернету или ошибкой на сервере.",
   "bundle_column_error.network.title": "Ошибка сети",
   "bundle_column_error.retry": "Попробовать снова",
   "bundle_column_error.return": "Вернуться на главную",
-  "bundle_column_error.routing.body": "Запрошенная страница не найдена. Вы уверены, что URL в адресной строке правильный?",
+  "bundle_column_error.routing.body": "Запрошенная страница не найдена. Вы уверены, что в адресной строке указан правильный URL?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Закрыть",
   "bundle_modal_error.message": "Что-то пошло не так при загрузке этой страницы.",
   "bundle_modal_error.retry": "Попробовать снова",
-  "closed_registrations.other_server_instructions": "Поскольку Mastodon децентрализован, вы можете создать учетную запись на другом сервере и всё ещё взаимодействовать с этим сервером.",
-  "closed_registrations_modal.description": "Создать учётную запись на {domain} сейчас не выйдет, но имейте в виду, что вам не нужна учётная запись именно на {domain}, чтобы использовать Mastodon.",
+  "closed_registrations.other_server_instructions": "Поскольку Mastodon децентрализован, вы можете зарегистрироваться на другом сервере и всё равно взаимодействовать с этим сервером.",
+  "closed_registrations_modal.description": "Зарегистрироваться на {domain} сейчас не выйдет, но имейте в виду, что вам не нужна учётная запись именно на {domain}, чтобы использовать Mastodon.",
   "closed_registrations_modal.find_another_server": "Найти другой сервер",
-  "closed_registrations_modal.preamble": "Mastodon децентрализован, поэтому независимо от того, где вы создадите свою учетную запись, вы сможете следить и взаимодействовать с кем угодно на этом сервере. Вы даже можете разместить свой собственный сервер!",
+  "closed_registrations_modal.preamble": "Mastodon децентрализован, поэтому независимо от того, где именно вы зарегистрируетесь, вы сможете подписываться и взаимодействовать с кем угодно на этом сервере. Вы даже можете создать свой собственный сервер!",
   "closed_registrations_modal.title": "Регистрация в Mastodon",
   "column.about": "О проекте",
   "column.blocks": "Заблокированные пользователи",
@@ -144,12 +154,12 @@
   "column.direct": "Личные упоминания",
   "column.directory": "Просмотр профилей",
   "column.domain_blocks": "Заблокированные домены",
-  "column.edit_list": "Изменить список",
-  "column.favourites": "Избранные",
+  "column.edit_list": "Редактировать список",
+  "column.favourites": "Избранное",
   "column.firehose": "Живая лента",
   "column.follow_requests": "Запросы на подписку",
   "column.home": "Главная",
-  "column.list_members": "Управление пользователями в списке",
+  "column.list_members": "Добавить или удалить из списка",
   "column.lists": "Списки",
   "column.mutes": "Игнорируемые пользователи",
   "column.notifications": "Уведомления",
@@ -166,58 +176,62 @@
   "column_subheading.settings": "Настройки",
   "community.column_settings.local_only": "Только локальные",
   "community.column_settings.media_only": "Только с медиафайлами",
-  "community.column_settings.remote_only": "Только удалённые",
+  "community.column_settings.remote_only": "Только с других серверов",
   "compose.language.change": "Изменить язык",
-  "compose.language.search": "Поиск языков...",
+  "compose.language.search": "Найти язык...",
   "compose.published.body": "Пост опубликован.",
   "compose.published.open": "Открыть",
   "compose.saved.body": "Пост отредактирован.",
   "compose_form.direct_message_warning_learn_more": "Узнать больше",
   "compose_form.encryption_warning": "Посты в Mastodon не защищены сквозным шифрованием. Не делитесь конфиденциальной информацией через Mastodon.",
-  "compose_form.hashtag_warning": "Этот пост не будет виден ни под одним из хэштегов, так как он не публичный. Только публичные посты можно найти по хэштегу.",
+  "compose_form.hashtag_warning": "Этот пост не появится в поиске по хэштегам, так как он не обозначен как публичный. Только публичные посты можно найти по хэштегу.",
   "compose_form.lock_disclaimer": "Ваша учётная запись {locked}. Любой пользователь сможет подписаться на вас и просматривать посты для подписчиков.",
   "compose_form.lock_disclaimer.lock": "не закрыта",
   "compose_form.placeholder": "О чём думаете?",
-  "compose_form.poll.duration": "Продолжительность опроса",
+  "compose_form.poll.duration": "Продолжительность",
   "compose_form.poll.multiple": "Несколько вариантов ответа",
   "compose_form.poll.option_placeholder": "Вариант {number}",
   "compose_form.poll.single": "Один вариант ответа",
-  "compose_form.poll.switch_to_multiple": "Разрешить выбор нескольких вариантов",
-  "compose_form.poll.switch_to_single": "Переключить в режим выбора одного ответа",
+  "compose_form.poll.switch_to_multiple": "Переключить в режим выбора нескольких вариантов ответа",
+  "compose_form.poll.switch_to_single": "Переключить в режим выбора одного варианта ответа",
   "compose_form.poll.type": "Тип",
   "compose_form.publish": "Опубликовать",
   "compose_form.publish_form": "Опубликовать",
   "compose_form.reply": "Ответить",
   "compose_form.save_changes": "Сохранить",
-  "compose_form.spoiler.marked": "Текст скрыт за предупреждением",
-  "compose_form.spoiler.unmarked": "Текст не скрыт",
-  "compose_form.spoiler_placeholder": "Предупреждение о содержимом (необязательно)",
+  "compose_form.spoiler.marked": "Удалить предупреждение о содержании",
+  "compose_form.spoiler.unmarked": "Добавить предупреждение о содержании",
+  "compose_form.spoiler_placeholder": "Предупреждение о содержании (необязательно)",
   "confirmation_modal.cancel": "Отмена",
   "confirmations.block.confirm": "Заблокировать",
   "confirmations.delete.confirm": "Удалить",
   "confirmations.delete.message": "Вы уверены, что хотите удалить этот пост?",
   "confirmations.delete.title": "Удалить пост?",
   "confirmations.delete_list.confirm": "Удалить",
-  "confirmations.delete_list.message": "Вы действительно хотите навсегда удалить этот список?",
+  "confirmations.delete_list.message": "Вы уверены, что хотите навсегда удалить этот список?",
   "confirmations.delete_list.title": "Удалить список?",
-  "confirmations.discard_edit_media.confirm": "Отменить",
-  "confirmations.discard_edit_media.message": "У вас имеются несохранённые изменения превью и описания медиафайла, отменить их?",
+  "confirmations.discard_edit_media.confirm": "Сбросить",
+  "confirmations.discard_edit_media.message": "У вас есть несохранённые изменения, касающиеся описания медиа или области предпросмотра, сбросить их?",
   "confirmations.edit.confirm": "Редактировать",
-  "confirmations.edit.message": "При редактировании, текст набираемого поста будет очищен. Продолжить?",
-  "confirmations.edit.title": "Переписать сообщение?",
-  "confirmations.follow_to_list.confirm": "Подписаться, а затем добавить в список",
+  "confirmations.edit.message": "Если вы начнёте редактировать сейчас, то набираемый в данный момент пост будет стёрт. Вы уверены, что хотите продолжить?",
+  "confirmations.edit.title": "Стереть несохранённый черновик поста?",
+  "confirmations.follow_to_list.confirm": "Подписаться и добавить",
   "confirmations.follow_to_list.message": "Чтобы добавить пользователя {name} в список, вы должны быть на него подписаны.",
-  "confirmations.follow_to_list.title": "Подписаться?",
+  "confirmations.follow_to_list.title": "Подписаться на пользователя?",
   "confirmations.logout.confirm": "Выйти",
   "confirmations.logout.message": "Вы уверены, что хотите выйти?",
   "confirmations.logout.title": "Выйти?",
+  "confirmations.missing_alt_text.confirm": "Добавить",
+  "confirmations.missing_alt_text.message": "Ваш пост содержит медиафайлы без альтернативного текста. Добавляя описания, вы делаете ваш контент доступным для более широкого круга людей.",
+  "confirmations.missing_alt_text.secondary": "Опубликовать",
+  "confirmations.missing_alt_text.title": "Добавить альтернативный текст?",
   "confirmations.mute.confirm": "Игнорировать",
   "confirmations.redraft.confirm": "Удалить и исправить",
-  "confirmations.redraft.message": "Вы уверены, что хотите удалить и переписать этот пост? Отметки «избранного», продвижения и ответы к оригинальному посту будут потеряны.",
-  "confirmations.redraft.title": "Создать пост заново?",
+  "confirmations.redraft.message": "Вы уверены, что хотите удалить этот пост и создать его заново? Взаимодействия, такие как добавление в избранное или продвижение, будут потеряны, а ответы к оригинальному посту перестанут на него ссылаться.",
+  "confirmations.redraft.title": "Удалить и создать пост заново?",
   "confirmations.reply.confirm": "Ответить",
-  "confirmations.reply.message": "При ответе, текст набираемого поста будет очищен. Продолжить?",
-  "confirmations.reply.title": "Перепишем пост?",
+  "confirmations.reply.message": "Если вы начнёте составлять ответ сейчас, то набираемый в данный момент пост будет стёрт. Вы уверены, что хотите продолжить?",
+  "confirmations.reply.title": "Стереть несохранённый черновик поста?",
   "confirmations.unfollow.confirm": "Отписаться",
   "confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
   "confirmations.unfollow.title": "Отписаться?",
@@ -231,7 +245,7 @@
   "copy_icon_button.copied": "Скопировано в буфер обмена",
   "copypaste.copied": "Скопировано",
   "copypaste.copy_to_clipboard": "Копировать в буфер обмена",
-  "directory.federated": "Со всей федерации",
+  "directory.federated": "Со всего федивёрса",
   "directory.local": "Только с {domain}",
   "directory.new_arrivals": "Новички",
   "directory.recently_active": "Недавно активные",
@@ -239,12 +253,12 @@
   "disabled_account_banner.text": "Ваша учётная запись {disabledAccount} в настоящее время отключена.",
   "dismissable_banner.community_timeline": "Это самые новые публичные посты от тех пользователей, чьи учётные записи находятся на сервере {domain}.",
   "dismissable_banner.dismiss": "Закрыть",
-  "dismissable_banner.explore_links": "Об этих новостях прямо сейчас говорят люди на этом и других серверах децентрализованной сети.",
-  "dismissable_banner.explore_statuses": "Эти посты привлекают людей на этом и других серверах децентрализованной сети прямо сейчас.",
-  "dismissable_banner.explore_tags": "Эти хэштеги привлекают людей на этом и других серверах децентрализованной сети прямо сейчас.",
-  "dismissable_banner.public_timeline": "Это самые новые публичные посты от тех пользователей этого и других серверов децентрализованной сети, на которых подписываются пользователи {domain}.",
+  "dismissable_banner.explore_links": "Об этих новостях говорят в федивёрсе прямо сейчас. Свежие новости, опубликованные несколькими разными людьми, ранжируются выше.",
+  "dismissable_banner.explore_statuses": "Эти посты со всего федивёрса прямо сейчас набирают популярность. Более новые посты с более высоким количеством взаимодействий ранжируются выше.",
+  "dismissable_banner.explore_tags": "Эти хэштеги набирают популярность в федивёрсе прямо сейчас. Хэштеги, используемые несколькими разными людьми, ранжируются выше.",
+  "dismissable_banner.public_timeline": "Это самые новые публичные посты от всех тех людей в федивёрсе, на которых подписаны пользователи {domain}.",
   "domain_block_modal.block": "Заблокировать сервер",
-  "domain_block_modal.block_account_instead": "Заблокировать только @{name}",
+  "domain_block_modal.block_account_instead": "Заблокировать @{name}",
   "domain_block_modal.they_can_interact_with_old_posts": "Пользователи с этого сервера смогут взаимодействовать с вашими старыми постами.",
   "domain_block_modal.they_cant_follow": "Пользователи с этого сервера не смогут подписаться на вас.",
   "domain_block_modal.they_wont_know": "Пользователи с этого сервера не будут знать, что вы их блокируете.",
@@ -282,15 +296,15 @@
   "emoji_button.search_results": "Результаты поиска",
   "emoji_button.symbols": "Символы",
   "emoji_button.travel": "Путешествия и места",
-  "empty_column.account_hides_collections": "Данный пользователь решил не предоставлять эту информацию",
-  "empty_column.account_suspended": "Учетная запись заблокирована",
+  "empty_column.account_hides_collections": "Пользователь предпочёл не раскрывать эту информацию",
+  "empty_column.account_suspended": "Учётная запись заблокирована",
   "empty_column.account_timeline": "Здесь нет постов!",
   "empty_column.account_unavailable": "Профиль недоступен",
   "empty_column.blocks": "Вы ещё никого не заблокировали.",
   "empty_column.bookmarked_statuses": "У вас пока нет закладок. Когда вы добавляете пост в закладки, он появляется здесь.",
   "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
   "empty_column.direct": "У вас пока нет личных сообщений. Как только вы отправите или получите сообщение, оно появится здесь.",
-  "empty_column.domain_blocks": "Скрытых доменов пока нет.",
+  "empty_column.domain_blocks": "Заблокированных доменов пока нет.",
   "empty_column.explore_statuses": "Нет актуального. Проверьте позже!",
   "empty_column.favourited_statuses": "Вы не добавили ни один пост в «Избранное». Как только вы это сделаете, он появится здесь.",
   "empty_column.favourites": "Никто ещё не добавил этот пост в «Избранное». Как только кто-то это сделает, это отобразится здесь.",
@@ -302,7 +316,7 @@
   "empty_column.mutes": "Вы ещё никого не добавляли в список игнорируемых.",
   "empty_column.notification_requests": "Здесь ничего нет! Когда вы получите новые уведомления, они здесь появятся согласно вашим настройкам.",
   "empty_column.notifications": "У вас пока нет уведомлений. Взаимодействуйте с другими, чтобы завести разговор.",
-  "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту",
+  "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других серверов, чтобы заполнить ленту",
   "error.unexpected_crash.explanation": "Из-за несовместимого браузера или ошибки в нашем коде, эта страница не может быть корректно отображена.",
   "error.unexpected_crash.explanation_addons": "Эта страница не может быть корректно отображена. Скорее всего, эта ошибка вызвана расширением браузера или инструментом автоматического перевода.",
   "error.unexpected_crash.next_steps": "Попробуйте обновить страницу. Если проблема не исчезает, используйте Mastodon из-под другого браузера или приложения.",
@@ -320,21 +334,21 @@
   "filter_modal.added.expired_title": "Истёкший фильтр!",
   "filter_modal.added.review_and_configure": "Для просмотра и настройки этой категории фильтра, перейдите в {settings_link}.",
   "filter_modal.added.review_and_configure_title": "Настройки фильтра",
-  "filter_modal.added.settings_link": "страница настроек",
+  "filter_modal.added.settings_link": "настройки",
   "filter_modal.added.short_explanation": "Этот пост был добавлен в следующую категорию фильтра: {title}.",
   "filter_modal.added.title": "Фильтр добавлен!",
   "filter_modal.select_filter.context_mismatch": "не применяется к этому контексту",
-  "filter_modal.select_filter.expired": "истекло",
+  "filter_modal.select_filter.expired": "истёкший",
   "filter_modal.select_filter.prompt_new": "Новая категория: {name}",
   "filter_modal.select_filter.search": "Поиск или создание",
   "filter_modal.select_filter.subtitle": "Используйте существующую категорию или создайте новую",
   "filter_modal.select_filter.title": "Фильтровать этот пост",
   "filter_modal.title.status": "Фильтровать пост",
   "filter_warning.matches_filter": "Соответствует фильтру \"<span>{title}</span>\"",
-  "filtered_notifications_banner.pending_requests": "Вы можете знать {count, plural, =0 {ни одного человека} one {одного человека} other {# человек}}",
+  "filtered_notifications_banner.pending_requests": "От {count, plural, =0 {не известных вам людей} one {# возможно вам известного человека} other {# возможно вам известных человек}}",
   "filtered_notifications_banner.title": "Отфильтрованные уведомления",
-  "firehose.all": "Все",
-  "firehose.local": "Текущий сервер",
+  "firehose.all": "Всё вместе",
+  "firehose.local": "Этот сервер",
   "firehose.remote": "Другие серверы",
   "follow_request.authorize": "Авторизовать",
   "follow_request.reject": "Отказать",
@@ -361,7 +375,7 @@
   "footer.keyboard_shortcuts": "Сочетания клавиш",
   "footer.privacy_policy": "Политика конфиденциальности",
   "footer.source_code": "Исходный код",
-  "footer.status": "Статус",
+  "footer.status": "Состояние сервера",
   "footer.terms_of_service": "Пользовательское соглашение",
   "generic.saved": "Сохранено",
   "getting_started.heading": "Добро пожаловать",
@@ -381,14 +395,14 @@
   "hashtag.follow": "Подписаться на новые посты",
   "hashtag.unfollow": "Отписаться",
   "hashtags.and_other": "…и {count, plural, other {ещё #}}",
-  "hints.profiles.followers_may_be_missing": "Подписчики у этого профиля могут отсутствовать.",
-  "hints.profiles.follows_may_be_missing": "Фолловеры для этого профиля могут отсутствовать.",
-  "hints.profiles.posts_may_be_missing": "Некоторые сообщения из этого профиля могут отсутствовать.",
-  "hints.profiles.see_more_followers": "Посмотреть больше подписчиков на {domain}",
-  "hints.profiles.see_more_follows": "Смотрите другие материалы по теме {domain}",
-  "hints.profiles.see_more_posts": "Посмотреть другие сообщения на {domain}",
+  "hints.profiles.followers_may_be_missing": "Подписчики этого профиля могут отсутствовать.",
+  "hints.profiles.follows_may_be_missing": "Подписки этого профиля могут отсутствовать.",
+  "hints.profiles.posts_may_be_missing": "Некоторые сообщения этого профиля могут отсутствовать.",
+  "hints.profiles.see_more_followers": "Перейдите на {domain}, чтобы увидеть всех подписчиков",
+  "hints.profiles.see_more_follows": "Перейдите на {domain}, чтобы увидеть все подписки",
+  "hints.profiles.see_more_posts": "Перейдите на {domain}, чтобы увидеть все посты",
   "hints.threads.replies_may_be_missing": "Ответы с других серверов могут отсутствовать.",
-  "hints.threads.see_more": "Посмотреть другие ответы на {domain}",
+  "hints.threads.see_more": "Перейдите на {domain}, чтобы увидеть все ответы",
   "home.column_settings.show_reblogs": "Показывать продвижения",
   "home.column_settings.show_replies": "Показывать ответы",
   "home.hide_announcements": "Скрыть объявления",
@@ -396,17 +410,19 @@
   "home.pending_critical_update.link": "Посмотреть обновления",
   "home.pending_critical_update.title": "Доступно критическое обновление безопасности!",
   "home.show_announcements": "Показать объявления",
-  "ignore_notifications_modal.disclaimer": "Mastodon не может сообщить пользователям, что вы проигнорировали их уведомления. Игнорирование уведомлений не остановит отправку самих сообщений.",
-  "ignore_notifications_modal.filter_instead": "Фильтр вместо",
-  "ignore_notifications_modal.filter_to_act_users": "Вы и далее сможете принять, отвергнуть и жаловаться на пользователей",
-  "ignore_notifications_modal.filter_to_avoid_confusion": "Фильтрация помогает избежать потенциальной путаницы",
-  "ignore_notifications_modal.filter_to_review_separately": "Вы можете просматривать отфильтрованные уведомления отдельно",
+  "ignore_notifications_modal.disclaimer": "Mastodon не может сообщить пользователям, что вы игнорируете их уведомления. Игнорирование уведомлений не остановит отправку самих сообщений.",
+  "ignore_notifications_modal.filter_instead": "Фильтровать",
+  "ignore_notifications_modal.filter_to_act_users": "Вы по-прежнему сможете принимать и отклонять запросы, а также отправлять жалобы на пользователей",
+  "ignore_notifications_modal.filter_to_avoid_confusion": "Фильтрация поможет избежать возможной путаницы",
+  "ignore_notifications_modal.filter_to_review_separately": "Отфильтрованные уведомления можно просматривать отдельно",
   "ignore_notifications_modal.ignore": "Игнорировать уведомления",
-  "ignore_notifications_modal.limited_accounts_title": "Игнорировать уведомления от модерируемых аккаунтов?",
-  "ignore_notifications_modal.new_accounts_title": "Игнорировать уведомления от новых аккаунтов?",
-  "ignore_notifications_modal.not_followers_title": "Игнорировать уведомления от людей, которые не следят за вами?",
-  "ignore_notifications_modal.not_following_title": "Игнорировать уведомления от людей, за которыми вы не следите?",
-  "ignore_notifications_modal.private_mentions_title": "Игнорировать уведомления о нежелательных личных сообщениях?",
+  "ignore_notifications_modal.limited_accounts_title": "Игнорировать уведомления от модерируемых учётных записей?",
+  "ignore_notifications_modal.new_accounts_title": "Игнорировать уведомления от новых учётных записей?",
+  "ignore_notifications_modal.not_followers_title": "Игнорировать уведомления от людей, не подписанных на вас?",
+  "ignore_notifications_modal.not_following_title": "Игнорировать уведомления от людей, на которых вы не подписаны?",
+  "ignore_notifications_modal.private_mentions_title": "Игнорировать уведомления о нежелательных личных упоминаниях?",
+  "info_button.label": "Помощь",
+  "info_button.what_is_alt_text": "<h1>Что это такое?</h1> <p>Альтернативный текст содержит описание изображения для людей с ограничениями зрения, медленным интернетом и для тех, кому нужен дополнительный контекст.</p> <p>Вы можете улучшить доступность и понимание для всех, написав четкий, краткий и объективный альтернативный текст.</p> <ul> <li>Уловите важные элементы</li> <li>Перескажите текстовую информацию на изображении</li> <li>Используйте правильную структуру предложений</li> <li>Избегайте избыточной информации</li> <li>Сосредоточьтесь на тенденциях и ключевых выводах при описании сложных визуализаций (таких как диаграммы или карты)</li> </ul>",
   "interaction_modal.action.favourite": "Вы можете добавить этот пост в избранное со своей учётной записью.",
   "interaction_modal.action.follow": "Вы можете подписаться со своей учётной записью.",
   "interaction_modal.action.reblog": "Вы можете продвинуть этот пост со своей учётной записью.",
@@ -457,6 +473,7 @@
   "keyboard_shortcuts.toggle_hidden": "показать/скрыть текст за предупреждением",
   "keyboard_shortcuts.toggle_sensitivity": "показать/скрыть медиафайлы",
   "keyboard_shortcuts.toot": "начать писать новый пост",
+  "keyboard_shortcuts.translate": "перевести пост",
   "keyboard_shortcuts.unfocus": "убрать фокус с поля ввода/поиска",
   "keyboard_shortcuts.up": "вверх по списку",
   "lightbox.close": "Закрыть",
@@ -517,9 +534,9 @@
   "navigation_bar.compose": "Создать новый пост",
   "navigation_bar.direct": "Личные упоминания",
   "navigation_bar.discover": "Изучайте",
-  "navigation_bar.domain_blocks": "Скрытые домены",
+  "navigation_bar.domain_blocks": "Заблокированные домены",
   "navigation_bar.explore": "Обзор",
-  "navigation_bar.favourites": "Избранные",
+  "navigation_bar.favourites": "Избранное",
   "navigation_bar.filters": "Игнорируемые слова",
   "navigation_bar.follow_requests": "Запросы на подписку",
   "navigation_bar.followed_tags": "Отслеживаемые хэштеги",
@@ -569,7 +586,7 @@
   "notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.",
   "notification.moderation_warning.action_suspend": "Действие вашей учётной записи приостановлено.",
   "notification.own_poll": "Ваш опрос завершился",
-  "notification.poll": "Голосование, в котором вы приняли участие, завершилось",
+  "notification.poll": "Опрос, в котором вы приняли участие, завершился",
   "notification.reblog": "{name} продвинул(а) ваш пост",
   "notification.reblog.name_and_others_with_link": "{name} и ещё <a>{count, plural, one {# пользователь} few {# пользователя} other {# пользователей}}</a> продвинули ваш пост",
   "notification.relationships_severance_event": "Потеряно соединение с {name}",
@@ -581,20 +598,20 @@
   "notification.update": "{name} изменил(а) пост",
   "notification_requests.accept": "Принять",
   "notification_requests.accept_multiple": "{count, plural, one {Принять # запрос…} few {Принять # запроса…} other {Принять # запросов…}}",
-  "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Принять запрос} other {Принять запросы}}",
+  "notification_requests.confirm_accept_multiple.button": "{count, plural, other {Принять запросы}}",
   "notification_requests.confirm_accept_multiple.message": "Вы собираетесь принять {count, plural, one {# запрос на показ уведомлений} few {# запроса на показ уведомлений} other {# запросов на показ уведомлений}}. Продолжить?",
-  "notification_requests.confirm_accept_multiple.title": "Принимать запросы на уведомления?",
-  "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Отклонить запрос} other {Отклонить запросы}}",
+  "notification_requests.confirm_accept_multiple.title": "Принять запросы на уведомления?",
+  "notification_requests.confirm_dismiss_multiple.button": "{count, plural, other {Отклонить запросы}}",
   "notification_requests.confirm_dismiss_multiple.message": "Вы собираетесь отклонить {count, plural, one {# запрос на показ уведомлений} few {# запроса на показ уведомлений} other {# запросов на показ уведомлений}}. Вы не сможете просмотреть {count, plural, other {их}} потом. Продолжить?",
-  "notification_requests.confirm_dismiss_multiple.title": "Отклонять запросы на уведомления?",
+  "notification_requests.confirm_dismiss_multiple.title": "Отклонить запросы на уведомления?",
   "notification_requests.dismiss": "Отклонить",
   "notification_requests.dismiss_multiple": "{count, plural, one {Отклонить # запрос…} few {Отклонить # запроса…} other {Отклонить # запросов…}}",
-  "notification_requests.edit_selection": "Редактировать",
+  "notification_requests.edit_selection": "Изменить",
   "notification_requests.exit_selection": "Готово",
-  "notification_requests.explainer_for_limited_account": "Уведомления от этой учетной записи были отфильтрованы, поскольку учетная запись была ограничена модератором.",
-  "notification_requests.explainer_for_limited_remote_account": "Уведомления от этой учетной записи были отфильтрованы, поскольку учетная запись или ее сервер были ограничены модератором.",
+  "notification_requests.explainer_for_limited_account": "Эта учётная запись ограничена модератором, поэтому уведомления от неё были отфильтрованы.",
+  "notification_requests.explainer_for_limited_remote_account": "Эта учётная запись или её сервер ограничены модератором, поэтому уведомления от неё были отфильтрованы.",
   "notification_requests.maximize": "Развернуть",
-  "notification_requests.minimize_banner": "Минимизация баннера отфильтрованных уведомлений",
+  "notification_requests.minimize_banner": "Свернуть панель отфильтрованных уведомлений в значок",
   "notification_requests.notifications_from": "Уведомления от {name}",
   "notification_requests.title": "Отфильтрованные уведомления",
   "notification_requests.view": "Просмотр уведомлений",
@@ -604,9 +621,9 @@
   "notifications.column_settings.admin.report": "Новые жалобы:",
   "notifications.column_settings.admin.sign_up": "Новые регистрации:",
   "notifications.column_settings.alert": "Уведомления на рабочем столе",
-  "notifications.column_settings.favourite": "Ваш пост добавили в избранные:",
+  "notifications.column_settings.favourite": "Ваш пост добавили в избранное:",
   "notifications.column_settings.filter_bar.advanced": "Показать все категории",
-  "notifications.column_settings.filter_bar.category": "Панель сортировки",
+  "notifications.column_settings.filter_bar.category": "Панель быстрых фильтров",
   "notifications.column_settings.follow": "У вас новый подписчик:",
   "notifications.column_settings.follow_request": "Новые запросы на подписку:",
   "notifications.column_settings.group": "Группировать",
@@ -619,11 +636,11 @@
   "notifications.column_settings.status": "Новые посты:",
   "notifications.column_settings.unread_notifications.category": "Непрочитанные уведомления",
   "notifications.column_settings.unread_notifications.highlight": "Выделять непрочитанные уведомления",
-  "notifications.column_settings.update": "Правки:",
+  "notifications.column_settings.update": "Пост был отредактирован:",
   "notifications.filter.all": "Все",
   "notifications.filter.boosts": "Продвижения",
   "notifications.filter.favourites": "Избранное",
-  "notifications.filter.follows": "Подписки",
+  "notifications.filter.follows": "Новые подписчики",
   "notifications.filter.mentions": "Упоминания",
   "notifications.filter.polls": "Результаты опросов",
   "notifications.filter.statuses": "Обновления от людей, на которых вы подписаны",
@@ -683,12 +700,12 @@
   "poll_button.remove_poll": "Удалить опрос",
   "privacy.change": "Изменить видимость поста",
   "privacy.direct.long": "Все упомянутые в посте",
-  "privacy.direct.short": "Определённые люди",
+  "privacy.direct.short": "Личное упоминание",
   "privacy.private.long": "Только ваши подписчики",
-  "privacy.private.short": "Подписчики",
-  "privacy.public.long": "Любой, находящийся на Mastodon и вне его",
+  "privacy.private.short": "Для подписчиков",
+  "privacy.public.long": "Кто угодно в интернете",
   "privacy.public.short": "Публичный",
-  "privacy.unlisted.additional": "Работает точно так же, как public, за исключением того, что пост не будет отображаться в прямых лентах, хэштегах, исследованиях или поиске Mastodon, даже если ваш аккаунт подписан на это на уровне всего аккаунта.",
+  "privacy.unlisted.additional": "Похоже на «Публичный» за исключением того, что пост не появится ни в живых лентах, ни в лентах хэштегов, ни в разделе «Обзор», ни в поиске Mastodon, даже если вы разрешили поиск по своим постам в настройках профиля.",
   "privacy.unlisted.long": "Меньше алгоритмических фанфар",
   "privacy.unlisted.short": "Тихий публичный",
   "privacy_policy.last_updated": "Последнее обновление {date}",
@@ -752,9 +769,8 @@
   "report.unfollow": "Отписаться от @{name}",
   "report.unfollow_explanation": "Вы подписаны на этого пользователя. Чтобы не видеть его/её посты в своей домашней ленте, отпишитесь от него/неё.",
   "report_notification.attached_statuses": "{count, plural, one {{count} сообщение} few {{count} сообщения} many {{count} сообщений} other {{count} сообщений}} вложено",
-  "report_notification.categories.legal": "Правовая информация",
-  "report_notification.categories.legal_sentence": "срамной контент",
-  "report_notification.categories.other": "Прочее",
+  "report_notification.categories.legal": "Нарушение закона",
+  "report_notification.categories.other": "Другое",
   "report_notification.categories.other_sentence": "другое",
   "report_notification.categories.spam": "Спам",
   "report_notification.categories.spam_sentence": "спам",
@@ -788,10 +804,10 @@
   "server_banner.about_active_users": "Люди, заходившие на этот сервер за последние 30 дней (ежемесячные активные пользователи)",
   "server_banner.active_users": "активные пользователи",
   "server_banner.administered_by": "Управляется:",
-  "server_banner.is_one_of_many": "{domain} — это один из многих независимых серверов Mastodon, которые вы можете использовать для участия в сети Fediverse.",
+  "server_banner.is_one_of_many": "{domain} — это один из многих независимых серверов Mastodon, которые вы можете использовать, чтобы присоединиться к сети Fediverse.",
   "server_banner.server_stats": "Статистика сервера:",
   "sign_in_banner.create_account": "Зарегистрироваться",
-  "sign_in_banner.follow_anyone": "Подписывайтесь на кого угодно в федивёрсе и смотрите ленту в хронологическом порядке. Никаких алгоритмов, рекламы или кликбейта.",
+  "sign_in_banner.follow_anyone": "Подписывайтесь на кого угодно в федивёрсе и читайте ленту в хронологическом порядке. Никаких алгоритмов, рекламы или кликбейта.",
   "sign_in_banner.mastodon_is": "Mastodon — лучший способ быть в курсе всего происходящего.",
   "sign_in_banner.sign_in": "Войти",
   "sign_in_banner.sso_redirect": "Войдите или Зарегистрируйтесь",
@@ -800,15 +816,15 @@
   "status.admin_status": "Открыть этот пост в интерфейсе модератора",
   "status.block": "Заблокировать @{name}",
   "status.bookmark": "Добавить в закладки",
-  "status.cancel_reblog_private": "Не продвигать",
+  "status.cancel_reblog_private": "Отменить продвижение",
   "status.cannot_reblog": "Этот пост не может быть продвинут",
-  "status.continued_thread": "Продолжение темы",
+  "status.continued_thread": "Продолжение треда",
   "status.copy": "Скопировать ссылку на пост",
   "status.delete": "Удалить",
   "status.detailed_status": "Подробный просмотр обсуждения",
   "status.direct": "Упомянуть @{name} лично",
   "status.direct_indicator": "Личное упоминание",
-  "status.edit": "Изменить",
+  "status.edit": "Редактировать",
   "status.edited": "Дата последнего изменения: {date}",
   "status.edited_x_times": "{count, plural, one {{count} изменение} many {{count} изменений} other {{count} изменения}}",
   "status.embed": "Встроить на свой сайт",
@@ -836,7 +852,8 @@
   "status.reblogs.empty": "Никто ещё не продвинул этот пост. Как только кто-то это сделает, они появятся здесь.",
   "status.redraft": "Создать заново",
   "status.remove_bookmark": "Убрать из закладок",
-  "status.replied_in_thread": "Ответил в теме",
+  "status.remove_favourite": "Убрать из избранного",
+  "status.replied_in_thread": "Ответил(а) в треде",
   "status.replied_to": "Ответил(а) {name}",
   "status.reply": "Ответить",
   "status.replyAll": "Ответить всем",
@@ -857,7 +874,9 @@
   "subscribed_languages.target": "Изменить языки подписки для {target}",
   "tabs_bar.home": "Главная",
   "tabs_bar.notifications": "Уведомления",
+  "terms_of_service.effective_as_of": "Действует с {date}",
   "terms_of_service.title": "Пользовательское соглашение",
+  "terms_of_service.upcoming_changes_on": "Предстоящие изменения {date}",
   "time_remaining.days": "{number, plural, one {остался # день} few {осталось # дня} many {осталось # дней} other {осталось # дней}}",
   "time_remaining.hours": "{number, plural, one {остался # час} few {осталось # часа} many {осталось # часов} other {осталось # часов}}",
   "time_remaining.minutes": "{number, plural, one {осталась # минута} few {осталось # минуты} many {осталось # минут} other {осталось # минут}}",
@@ -878,7 +897,7 @@
   "upload_form.drag_and_drop.on_drag_end": "Медиа вложение {item} было удалено.",
   "upload_form.drag_and_drop.on_drag_over": "Медиа вложение {item} было перемещено.",
   "upload_form.drag_and_drop.on_drag_start": "Загружается медиафайл {item}.",
-  "upload_form.edit": "Изменить",
+  "upload_form.edit": "Редактировать",
   "upload_progress.label": "Загрузка...",
   "upload_progress.processing": "Обработка…",
   "username.taken": "Это имя пользователя уже занято. Выберите другое",
@@ -891,5 +910,9 @@
   "video.mute": "Выключить звук",
   "video.pause": "Пауза",
   "video.play": "Пуск",
-  "video.unmute": "Включить звук"
+  "video.skip_backward": "Промотать назад",
+  "video.skip_forward": "Промотать вперёд",
+  "video.unmute": "Включить звук",
+  "video.volume_down": "Уменьшить громкость",
+  "video.volume_up": "Увеличить громкость"
 }
diff --git a/app/javascript/mastodon/locales/ry.json b/app/javascript/mastodon/locales/ry.json
index 8fd083efc3..ed9751634e 100644
--- a/app/javascript/mastodon/locales/ry.json
+++ b/app/javascript/mastodon/locales/ry.json
@@ -28,7 +28,6 @@
   "account.endorse": "Указовати на профілови",
   "account.featured_tags.last_status_at": "Датум послідньої публикації {date}",
   "account.featured_tags.last_status_never": "Ниє публикацій",
-  "account.featured_tags.title": "Ублюблені гештеґы {name}",
   "account.follow": "Пудписати ся",
   "account.follow_back": "Пудписати ся тоже",
   "account.followers": "Пудписникы",
diff --git a/app/javascript/mastodon/locales/sa.json b/app/javascript/mastodon/locales/sa.json
index 1ecc057023..ce88bda740 100644
--- a/app/javascript/mastodon/locales/sa.json
+++ b/app/javascript/mastodon/locales/sa.json
@@ -26,7 +26,6 @@
   "account.endorse": "व्यक्तिगतविवरणे वैशिष्ट्यम्",
   "account.featured_tags.last_status_at": "{date} दिने गतस्थापनम्",
   "account.featured_tags.last_status_never": "न पत्रम्",
-  "account.featured_tags.title": "{name} इत्यस्य विशेषहैस्टैगः",
   "account.follow": "अनुस्रियताम्",
   "account.followers": "अनुसर्तारः",
   "account.followers.empty": "नाऽनुसर्तारो वर्तन्ते",
diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json
index 27080aaa9e..79ef6f6ef5 100644
--- a/app/javascript/mastodon/locales/sc.json
+++ b/app/javascript/mastodon/locales/sc.json
@@ -29,7 +29,6 @@
   "account.endorse": "Cussìgia in su profilu tuo",
   "account.featured_tags.last_status_at": "Ùrtima publicatzione in su {date}",
   "account.featured_tags.last_status_never": "Peruna publicatzione",
-  "account.featured_tags.title": "Etichetas de {name} in evidèntzia",
   "account.follow": "Sighi",
   "account.follow_back": "Sighi tue puru",
   "account.followers": "Sighiduras",
@@ -538,7 +537,6 @@
   "poll_button.remove_poll": "Cantzella su sondàgiu",
   "privacy.change": "Modìfica s'istadu de riservadesa",
   "privacy.direct.long": "Totu is utentes mentovados in sa publicatzione",
-  "privacy.direct.short": "Persones ispetzìficas",
   "privacy.private.long": "Isceti chie ti sighit",
   "privacy.private.short": "Sighiduras",
   "privacy.public.long": "Cale si siat persone a intro o a foras de Mastodon",
@@ -690,8 +688,6 @@
   "video.expand": "Ismànnia su vìdeu",
   "video.fullscreen": "Ischermu in mannària prena",
   "video.hide": "Cua vìdeu",
-  "video.mute": "A sa muda",
   "video.pause": "Pàusa",
-  "video.play": "Reprodue",
-  "video.unmute": "Ativa sonu"
+  "video.play": "Reprodue"
 }
diff --git a/app/javascript/mastodon/locales/sco.json b/app/javascript/mastodon/locales/sco.json
index 6adf365e52..7a7f926d5a 100644
--- a/app/javascript/mastodon/locales/sco.json
+++ b/app/javascript/mastodon/locales/sco.json
@@ -1,7 +1,7 @@
 {
   "about.blocks": "Moderatit servers",
   "about.contact": "Contack:",
-  "about.disclaimer": "Mastodon is free, open-soorced saftware, an a trademairk o Mastodon gGmbH.",
+  "about.disclaimer": "Join the Fediverse, become part of a community, and break free from Big Tech™'s stranglehold on public discourse.",
   "about.domain_blocks.no_reason_available": "Raison no available",
   "about.domain_blocks.preamble": "On the hail, Mastodon lats ye view content frae an interack wi uisers fae onie ither server in the fediverse.",
   "about.domain_blocks.silenced.explanation": "Ye'll generally no see profiles an content frae this server, unless ye explicitly luik it up or opt intae it bi follaein.",
@@ -25,7 +25,6 @@
   "account.endorse": "Shaw oan profile",
   "account.featured_tags.last_status_at": "Last post oan {date}",
   "account.featured_tags.last_status_never": "Nae posts",
-  "account.featured_tags.title": "{name}'s hielichtit hashtags",
   "account.follow": "Follae",
   "account.followers": "Follaers",
   "account.followers.empty": "Naebody follaes this uiser yit.",
@@ -509,8 +508,6 @@
   "video.expand": "Expand video",
   "video.fullscreen": "Fu-screen",
   "video.hide": "Plank video",
-  "video.mute": "Wheesht soond",
   "video.pause": "Pause",
-  "video.play": "Pley",
-  "video.unmute": "Unwheesht soond"
+  "video.play": "Pley"
 }
diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json
index 7e2d9af986..7d909dbe1a 100644
--- a/app/javascript/mastodon/locales/si.json
+++ b/app/javascript/mastodon/locales/si.json
@@ -445,8 +445,6 @@
   "video.expand": "දෘශ්‍යකය විහිදන්න",
   "video.fullscreen": "පූර්ණ තිරය",
   "video.hide": "දෘශ්‍යකය සඟවන්න",
-  "video.mute": "ශබ්දය නිහඬ",
   "video.pause": "විරාමය",
-  "video.play": "ධාවනය",
-  "video.unmute": "ශබ්දය නොනිහඬ"
+  "video.play": "ධාවනය"
 }
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 5e7385bdc4..6cbf799328 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -27,9 +27,9 @@
   "account.edit_profile": "Upraviť profil",
   "account.enable_notifications": "Zapnúť upozornenia na príspevky od @{name}",
   "account.endorse": "Zobraziť na vlastnom profile",
+  "account.featured.posts": "Príspevky",
   "account.featured_tags.last_status_at": "Posledný príspevok dňa {date}",
   "account.featured_tags.last_status_never": "Žiadne príspevky",
-  "account.featured_tags.title": "Odporúčané hashtagy účtu {name}",
   "account.follow": "Sledovať",
   "account.follow_back": "Sledovať späť",
   "account.followers": "Sledovatelia",
@@ -65,6 +65,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} príspevok} other {{counter} príspevkov}}",
   "account.unblock": "Odblokovať @{name}",
   "account.unblock_domain": "Odblokovať doménu {domain}",
+  "account.unblock_domain_short": "Odblokovať",
   "account.unblock_short": "Odblokovať",
   "account.unendorse": "Nezobrazovať na vlastnom profile",
   "account.unfollow": "Zrušiť sledovanie",
@@ -205,6 +206,7 @@
   "confirmations.logout.confirm": "Odhlásiť sa",
   "confirmations.logout.message": "Určite sa chcete odhlásiť?",
   "confirmations.logout.title": "Odhlásiť sa?",
+  "confirmations.missing_alt_text.secondary": "Odošli aj tak",
   "confirmations.mute.confirm": "Stíšiť",
   "confirmations.redraft.confirm": "Vymazať a prepísať",
   "confirmations.redraft.message": "Určite chcete tento príspevok vymazať a prepísať? Prídete o jeho zdieľania a ohviezdičkovania a odpovede na pôvodný príspevok budú odlúčené.",
@@ -262,6 +264,7 @@
   "emoji_button.search_results": "Výsledky hľadania",
   "emoji_button.symbols": "Symboly",
   "emoji_button.travel": "Cestovanie a miesta",
+  "empty_column.account_featured": "Tento zoznam je prázdny",
   "empty_column.account_hides_collections": "Tento účet sa rozhodol túto informáciu nesprístupniť",
   "empty_column.account_suspended": "Účet bol pozastavený",
   "empty_column.account_timeline": "Nie sú tu žiadne príspevky.",
@@ -319,6 +322,7 @@
   "follow_requests.unlocked_explanation": "Aj keď váš účet nie je uzamknutý, tím domény {domain} si myslel, že môžete chcieť skontrolovať žiadosti o sledovanie z týchto účtov manuálne.",
   "follow_suggestions.curated_suggestion": "Výber redakcie",
   "follow_suggestions.dismiss": "Znova nezobrazovať",
+  "follow_suggestions.featured_longer": "Ručne vybrané tímom {domain}",
   "follow_suggestions.friends_of_friends_longer": "Populárne medzi ľudmi ktorých nasleduješ",
   "follow_suggestions.hints.featured": "Tento profil bol ručne zvolený tímom domény {domain}.",
   "follow_suggestions.hints.friends_of_friends": "Tento profil je obľúbený medzi účtami, ktoré sledujete.",
@@ -343,6 +347,7 @@
   "generic.saved": "Uložené",
   "getting_started.heading": "Začíname",
   "hashtag.admin_moderation": "Otvor moderovacie rozhranie pre #{name}",
+  "hashtag.browse": "Prehľadávať príspevky pod #{hashtag}",
   "hashtag.column_header.tag_mode.all": "a {additional}",
   "hashtag.column_header.tag_mode.any": "alebo {additional}",
   "hashtag.column_header.tag_mode.none": "bez {additional}",
@@ -356,6 +361,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} príspevok} few {{counter} príspevky} many {{counter} príspevkov} other {{counter} príspevkov}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} príspevok} few {{counter} príspevky} many {{counter} príspevkov} other {{counter} príspevkov}} dnes",
   "hashtag.follow": "Sledovať hashtag",
+  "hashtag.mute": "Utlmiť #{hashtag}",
   "hashtag.unfollow": "Prestať sledovať hashtag",
   "hashtags.and_other": "…a {count, plural, other {# ďalších}}",
   "hints.profiles.followers_may_be_missing": "Nasledovatelia tohto profilu môžu chýbať.",
@@ -377,6 +383,7 @@
   "ignore_notifications_modal.filter_to_act_users": "Stále budeš môcť akceptovať, odmietnuť, alebo nahlásiť užívateľov",
   "ignore_notifications_modal.filter_to_avoid_confusion": "Triedenie pomáha vyvarovať sa možnému zmäteniu",
   "ignore_notifications_modal.ignore": "Ignoruj upozornenia",
+  "ignore_notifications_modal.limited_accounts_title": "Ignorovať oboznámenia z obmedzených účtov?",
   "ignore_notifications_modal.new_accounts_title": "Nevšímať si oznámenia z nových účtov?",
   "ignore_notifications_modal.not_followers_title": "Nevšímať si oznámenia od ľudí, ktorí ťa nenasledujú?",
   "ignore_notifications_modal.not_following_title": "Nevšímať si oznámenia od ľudí, ktorých nenasleduješ?",
@@ -452,6 +459,8 @@
   "lists.delete": "Vymazať zoznam",
   "lists.done": "Hotovo",
   "lists.edit": "Upraviť zoznam",
+  "lists.exclusive": "Skryť členov na Domovskej osi",
+  "lists.exclusive_hint": "Ak je niekto na tomto zozname, skry ich na tvojej Domácej osi, aby si ich príspevky nevidel/a dvakrát.",
   "lists.find_users_to_add": "Nájdi užívateľov na pridanie",
   "lists.list_members": "Členovia zoznamu",
   "lists.list_name": "Názov zoznamu",
@@ -465,6 +474,7 @@
   "lists.replies_policy.none": "Nikomu",
   "lists.save": "Ulož",
   "lists.search": "Hľadaj",
+  "lists.show_replies_to": "Zahrnúť odpovede od členov zoznamu na",
   "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položiek} other {# nových položiek}}",
   "loading_indicator.label": "Načítavanie…",
   "media_gallery.hide": "Skryť",
@@ -598,6 +608,7 @@
   "onboarding.follows.done": "Hotovo",
   "onboarding.follows.empty": "Žiaľ, momentálne sa nedajú zobraziť žiadne výsledky. Môžete skúsiť použiť vyhľadávanie alebo navštíviť stránku objavovania a nájsť ľudí, ktorých chcete sledovať, alebo to skúste znova neskôr.",
   "onboarding.follows.search": "Hľadať",
+  "onboarding.follows.title": "Pre začiatok nasleduj ľudí",
   "onboarding.profile.discoverable": "Nastavte svoj profil ako objaviteľný",
   "onboarding.profile.discoverable_hint": "Keď si na Mastodone zapnete objaviteľnosť, vaše príspevky sa môžu zobrazovať vo výsledkoch vyhľadávania a v populárnych. Váš profil môže byť navyše navrhovaný ľuďom, s ktorými máte podobné záujmy.",
   "onboarding.profile.display_name": "Používateľské meno",
@@ -623,7 +634,7 @@
   "poll_button.remove_poll": "Odstrániť anketu",
   "privacy.change": "Zmeniť nastavenia súkromia príspevku",
   "privacy.direct.long": "Všetci spomenutí v príspevku",
-  "privacy.direct.short": "Konkrétni ľudia",
+  "privacy.direct.short": "Spomenutie v súkromí",
   "privacy.private.long": "Iba vaši sledovatelia",
   "privacy.private.short": "Sledovatelia",
   "privacy.public.long": "Ktokoľvek na Mastodone aj mimo neho",
@@ -730,6 +741,7 @@
   "server_banner.is_one_of_many": "{domain} je jeden z mnohých nezávislých Mastodon serverov, ktoré môžeš použiť na zúčastňovanie sa v rámci fediversa.",
   "server_banner.server_stats": "Štatistiky servera:",
   "sign_in_banner.create_account": "Vytvoriť účet",
+  "sign_in_banner.mastodon_is": "Mastodon je najlepšia cesta ako udržať krok s tým, čo sa deje.",
   "sign_in_banner.sign_in": "Prihlásiť sa",
   "sign_in_banner.sso_redirect": "Prihlásenie alebo registrácia",
   "status.admin_account": "Moderovať @{name}",
@@ -818,8 +830,9 @@
   "video.expand": "Zväčšiť video",
   "video.fullscreen": "Zobraziť na celú obrazovku",
   "video.hide": "Skryť video",
-  "video.mute": "Stlmiť zvuk",
+  "video.mute": "Stíšiť",
   "video.pause": "Pozastaviť",
   "video.play": "Prehrať",
-  "video.unmute": "Zapnúť zvuk"
+  "video.volume_down": "Hlasitosť nadol",
+  "video.volume_up": "Hlasitosť nahor"
 }
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index 3fcb1f9dbc..eef20456de 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -1,10 +1,10 @@
 {
   "about.blocks": "Moderirani strežniki",
   "about.contact": "Stik:",
-  "about.disclaimer": "Mastodon je prosto, odprto-kodno programje in blagovna znamka Mastodon gGmbH.",
+  "about.disclaimer": "Mastodon je prosto, odprtokodno programje in blagovna znamka podjetja Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Razlog ni na voljo",
-  "about.domain_blocks.preamble": "Mastodon vam splošno omogoča ogled vsebin in interakcijo z uporabniki iz vseh drugih strežnikov v fediverzumu. To so izjeme, opravljene na tem strežniku.",
-  "about.domain_blocks.silenced.explanation": "V splošnem ne boste videli profilov in vsebin s tega strežnika, če jih eksplicino ne poiščete ali nanje naročite s sledenjem.",
+  "about.domain_blocks.preamble": "Mastodon vam na splošno omogoča ogled vsebin in interakcijo z uporabniki z vseh drugih strežnikov v fediverzumu. Tu so navedene izjeme, ki jih postavlja ta strežnik.",
+  "about.domain_blocks.silenced.explanation": "V splošnem ne boste videli profilov in vsebin s tega strežnika, razen če jih izrecno poiščete ali jim začnete slediti.",
   "about.domain_blocks.silenced.title": "Omejeno",
   "about.domain_blocks.suspended.explanation": "Nobeni podatki s tega strežnika ne bodo obdelani, shranjeni ali izmenjani, zaradi česar je nemogoča kakršna koli interakcija ali komunikacija z uporabniki s tega strežnika.",
   "about.domain_blocks.suspended.title": "Suspendiran",
@@ -29,11 +29,10 @@
   "account.endorse": "Izpostavi v profilu",
   "account.featured_tags.last_status_at": "Zadnja objava {date}",
   "account.featured_tags.last_status_never": "Ni objav",
-  "account.featured_tags.title": "Izpostavljeni ključniki {name}",
   "account.follow": "Sledi",
   "account.follow_back": "Sledi nazaj",
   "account.followers": "Sledilci",
-  "account.followers.empty": "Nihče ne sledi temu uporabniku.",
+  "account.followers.empty": "Nihče še ne sledi temu uporabniku.",
   "account.followers_counter": "{count, plural, one {{counter} sledilec} two {{counter} sledilca} few {{counter} sledilci} other {{counter} sledilcev}}",
   "account.following": "Sledim",
   "account.following_counter": "{count, plural, one {{counter} sleden} two {{counter} sledena} few {{counter} sledeni} other {{counter} sledenih}}",
@@ -45,9 +44,9 @@
   "account.languages": "Spremeni naročene jezike",
   "account.link_verified_on": "Lastništvo te povezave je bilo preverjeno {date}",
   "account.locked_info": "Stanje zasebnosti računa je nastavljeno na zaklenjeno. Lastnik ročno pregleda, kdo ga lahko spremlja.",
-  "account.media": "Mediji",
+  "account.media": "Predstavnosti",
   "account.mention": "Omeni @{name}",
-  "account.moved_to": "{name} nakazuje, da ima zdaj nov račun:",
+  "account.moved_to": "{name} sporoča, da ima zdaj nov račun:",
   "account.mute": "Utišaj @{name}",
   "account.mute_notifications_short": "Utišaj obvestila",
   "account.mute_short": "Utišaj",
@@ -68,14 +67,14 @@
   "account.unblock_short": "Odblokiraj",
   "account.unendorse": "Ne vključi v profil",
   "account.unfollow": "Ne sledi več",
-  "account.unmute": "Odtišaj @{name}",
+  "account.unmute": "Povrni glas @{name}",
   "account.unmute_notifications_short": "Izklopi utišanje obvestil",
-  "account.unmute_short": "Odtišaj",
-  "account_note.placeholder": "Kliknite za dodajanje opombe",
+  "account.unmute_short": "Povrni glas",
+  "account_note.placeholder": "Kliknite, da dodate opombo",
   "admin.dashboard.daily_retention": "Mera ohranjanja uporabnikov po dnevih od registracije",
   "admin.dashboard.monthly_retention": "Mera ohranjanja uporabnikov po mesecih od registracije",
   "admin.dashboard.retention.average": "Povprečje",
-  "admin.dashboard.retention.cohort": "Mesec prijave",
+  "admin.dashboard.retention.cohort": "Mesec registracije",
   "admin.dashboard.retention.cohort_size": "Novi uporabniki",
   "admin.impact_report.instance_accounts": "Profili računov, ki bi jih s tem izbrisali",
   "admin.impact_report.instance_followers": "Sledilci, ki bi jih izgubili naši uporabniki",
@@ -87,44 +86,61 @@
   "alert.unexpected.title": "Ojoj!",
   "alt_text_badge.title": "Nadomestno besedilo",
   "alt_text_modal.add_alt_text": "Dodaj nadomestno besedilo",
+  "alt_text_modal.add_text_from_image": "Dodaj besedilo iz slike",
   "alt_text_modal.cancel": "Prekliči",
   "alt_text_modal.change_thumbnail": "Spremeni sličico",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Podaj opis za ljudi s težavami s sluhom ...",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Podaj opis za slabovidne ...",
   "alt_text_modal.done": "Opravljeno",
-  "announcement.announcement": "Obvestilo",
+  "announcement.announcement": "Oznanilo",
+  "annual_report.summary.archetype.booster": "Lovec na trende",
+  "annual_report.summary.archetype.lurker": "Tiholazec",
+  "annual_report.summary.archetype.oracle": "Orakelj",
+  "annual_report.summary.archetype.pollster": "Anketar",
+  "annual_report.summary.archetype.replier": "Priljudnež",
   "annual_report.summary.followers.followers": "sledilcev",
-  "annual_report.summary.followers.total": "",
+  "annual_report.summary.followers.total": "skupaj {count}",
+  "annual_report.summary.here_it_is": "Tule je povzetek vašega leta {year}:",
   "annual_report.summary.highlighted_post.by_favourites": "- najpriljubljenejša objava",
+  "annual_report.summary.highlighted_post.by_reblogs": "- največkrat izpostavljena objava",
+  "annual_report.summary.highlighted_post.by_replies": "- objava z največ odgovori",
+  "annual_report.summary.highlighted_post.possessive": "{name}",
+  "annual_report.summary.most_used_app.most_used_app": "najpogosteje uporabljena aplikacija",
+  "annual_report.summary.most_used_hashtag.most_used_hashtag": "največkrat uporabljen ključnik",
   "annual_report.summary.most_used_hashtag.none": "Brez",
   "annual_report.summary.new_posts.new_posts": "nove objave",
+  "annual_report.summary.percentile.text": "<topLabel>S tem ste se uvrstili med zgornjih </topLabel><percentage></percentage><bottomLabel> uporabnikov domene {domain}.</bottomLabel>",
+  "annual_report.summary.percentile.we_wont_tell_bernie": "Živi duši ne bomo povedali.",
   "annual_report.summary.thanks": "Hvala, ker ste del Mastodona!",
   "attachments_list.unprocessed": "(neobdelano)",
   "audio.hide": "Skrij zvok",
-  "block_modal.remote_users_caveat": "Od strežnika {domain} bomo zahtevali, da spoštuje vašo odločitev. Izpolnjevanje zahteve ni zagotovljeno, ker nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.",
+  "block_modal.remote_users_caveat": "Strežnik {domain} bomo pozvali, naj spoštuje vašo odločitev. Kljub temu pa ni gotovo, da bo strežnik prošnjo upošteval, saj nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.",
   "block_modal.show_less": "Pokaži manj",
   "block_modal.show_more": "Pokaži več",
-  "block_modal.they_cant_mention": "Ne morejo vas omenjati ali vam slediti.",
-  "block_modal.they_cant_see_posts": "Ne vidijo vaših objav, vi pa ne njihovih.",
-  "block_modal.they_will_know": "Ne morejo videti, da so blokirani.",
-  "block_modal.title": "Blokiraj uporabnika?",
-  "block_modal.you_wont_see_mentions": "Objav, ki jih omenjajo, ne boste videli.",
-  "boost_modal.combo": "Če želite preskočiti to, lahko pritisnete {combo}",
-  "boost_modal.reblog": "Izpostavi objavo?",
+  "block_modal.they_cant_mention": "Ne more vas omenjati ali vam slediti.",
+  "block_modal.they_cant_see_posts": "Ne vidi vaših objav, vi pa ne njegovih.",
+  "block_modal.they_will_know": "Ne more videti, da je blokiran.",
+  "block_modal.title": "Blokiram uporabnika?",
+  "block_modal.you_wont_see_mentions": "Objav, ki ga omenjajo, ne boste videli.",
+  "boost_modal.combo": "Če želite naslednjič to preskočiti, lahko pritisnete {combo}",
+  "boost_modal.reblog": "Izpostavim objavo?",
   "boost_modal.undo_reblog": "Ali želite preklicati izpostavitev objave?",
   "bundle_column_error.copy_stacktrace": "Kopiraj poročilo o napaki",
   "bundle_column_error.error.body": "Zahtevane strani ni mogoče upodobiti. Vzrok težave je morda hrošč v naši kodi ali pa nezdružljivost z brskalnikom.",
   "bundle_column_error.error.title": "Oh, ne!",
   "bundle_column_error.network.body": "Pri poskusu nalaganja te strani je prišlo do napake. Vzrok je lahko začasna težava z vašo internetno povezavo ali s tem strežnikom.",
-  "bundle_column_error.network.title": "Napaka v omrežju",
+  "bundle_column_error.network.title": "Omrežna napaka",
   "bundle_column_error.retry": "Poskusi znova",
   "bundle_column_error.return": "Nazaj domov",
   "bundle_column_error.routing.body": "Zahtevane strani ni mogoče najti. Ali ste prepričani, da je naslov URL v naslovni vrstici pravilen?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Zapri",
+  "bundle_modal_error.message": "Med nalaganjem prikaza je prišlo do napake.",
   "bundle_modal_error.retry": "Poskusi znova",
   "closed_registrations.other_server_instructions": "Ker je Mastodon decentraliziran, lahko ustvarite račun na drugem strežniku in ste še vedno v interakciji s tem.",
-  "closed_registrations_modal.description": "Odpiranje računa na {domain} trenutno ni možno, upoštevajte pa, da ne potrebujete računa prav na {domain}, da bi uporabljali Mastodon.",
+  "closed_registrations_modal.description": "Odpiranje računa na domeni {domain} trenutno ni možno, upoštevajte pa, da ne potrebujete računa prav na domeni {domain}, da bi uporabljali Mastodon.",
   "closed_registrations_modal.find_another_server": "Najdi drug strežnik",
-  "closed_registrations_modal.preamble": "Mastodon je decentraliziran, kar pomeni, da ni pomembno, kje ustvarite svoj račun; od koder koli je omogočeno sledenje in interakcija z vsemi s tega strežnika. Strežnik lahko gostite tudi sami!",
+  "closed_registrations_modal.preamble": "Mastodon je decentraliziran, kar pomeni, da ni pomembno, kje ustvarite svoj račun; od koder koli je mogoče slediti in komunicirati z vsemi s tega strežnika. Strežnik lahko gostite tudi sami!",
   "closed_registrations_modal.title": "Registracija v Mastodon",
   "column.about": "O programu",
   "column.blocks": "Blokirani uporabniki",
@@ -137,7 +153,7 @@
   "column.edit_list": "Uredi seznam",
   "column.favourites": "Priljubljeni",
   "column.firehose": "Viri v živo",
-  "column.follow_requests": "Sledi prošnjam",
+  "column.follow_requests": "Prošnje za sledenje",
   "column.home": "Domov",
   "column.list_members": "Upravljaj člane seznama",
   "column.lists": "Seznami",
@@ -155,25 +171,25 @@
   "column_search.cancel": "Prekliči",
   "column_subheading.settings": "Nastavitve",
   "community.column_settings.local_only": "Samo krajevno",
-  "community.column_settings.media_only": "Samo mediji",
+  "community.column_settings.media_only": "Samo predstavnosti",
   "community.column_settings.remote_only": "Samo oddaljeno",
   "compose.language.change": "Spremeni jezik",
-  "compose.language.search": "Poišči jezik ...",
+  "compose.language.search": "Poišči jezike ...",
   "compose.published.body": "Objavljeno.",
   "compose.published.open": "Odpri",
   "compose.saved.body": "Objava shranjena.",
-  "compose_form.direct_message_warning_learn_more": "Izvej več",
-  "compose_form.encryption_warning": "Objave na Mastodonu niso šifrirane od kraja do kraja. Prek Mastodona ne delite nobenih občutljivih informacij.",
+  "compose_form.direct_message_warning_learn_more": "Več o tem",
+  "compose_form.encryption_warning": "Objave na Mastodonu niso šifrirane od konca do konca. Prek Mastodona ne delite nobenih občutljivih informacij.",
   "compose_form.hashtag_warning": "Ta objava ne bo navedena pod nobenim ključnikom, ker ni javna. Samo javne objave lahko iščete s ključniki.",
   "compose_form.lock_disclaimer": "Vaš račun ni {locked}. Vsakdo vam lahko sledi in si ogleda objave, ki so namenjene samo sledilcem.",
   "compose_form.lock_disclaimer.lock": "zaklenjen",
   "compose_form.placeholder": "O čem razmišljate?",
   "compose_form.poll.duration": "Trajanje ankete",
-  "compose_form.poll.multiple": "Več možnosti",
+  "compose_form.poll.multiple": "Izbira več možnosti",
   "compose_form.poll.option_placeholder": "Možnost {number}",
-  "compose_form.poll.single": "Ena izbira",
-  "compose_form.poll.switch_to_multiple": "Spremenite anketo, da omogočite več izbir",
-  "compose_form.poll.switch_to_single": "Spremenite anketo, da omogočite eno izbiro",
+  "compose_form.poll.single": "Izbira ene možnosti",
+  "compose_form.poll.switch_to_multiple": "Spremenite anketo, da omogočite izbiro več možnosti",
+  "compose_form.poll.switch_to_single": "Spremenite anketo, da omogočite izbiro ene možnosti",
   "compose_form.poll.type": "Slog",
   "compose_form.publish": "Objavi",
   "compose_form.publish_form": "Objavi",
@@ -191,17 +207,24 @@
   "confirmations.delete_list.message": "Ali ste prepričani, da želite trajno izbrisati ta seznam?",
   "confirmations.delete_list.title": "Želite izbrisati seznam?",
   "confirmations.discard_edit_media.confirm": "Opusti",
-  "confirmations.discard_edit_media.message": "Imate ne shranjene spremembe za medijski opis ali predogled; jih želite kljub temu opustiti?",
+  "confirmations.discard_edit_media.message": "Spremenjenega opisa predstavnosti ali predogleda niste shranili. Želite spremembe kljub temu opustiti?",
   "confirmations.edit.confirm": "Uredi",
   "confirmations.edit.message": "Urejanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?",
   "confirmations.edit.title": "Želite prepisati objavo?",
+  "confirmations.follow_to_list.confirm": "Sledi in dodaj na seznam",
+  "confirmations.follow_to_list.message": "Osebi {name} morate slediti, preden jo lahko dodate na seznam.",
+  "confirmations.follow_to_list.title": "Naj sledim uporabniku?",
   "confirmations.logout.confirm": "Odjava",
   "confirmations.logout.message": "Ali ste prepričani, da se želite odjaviti?",
   "confirmations.logout.title": "Se želite odjaviti?",
-  "confirmations.mute.confirm": "Utišanje",
+  "confirmations.missing_alt_text.confirm": "Dodaj nadomestno besedilo",
+  "confirmations.missing_alt_text.message": "Vaša objava vsebuje predstavnosti brez nadomestnega besedila. Če jih dodatno opišete, bodo dostopne večji množici ljudi.",
+  "confirmations.missing_alt_text.secondary": "Vseeno objavi",
+  "confirmations.missing_alt_text.title": "Dodam nadomestno besedilo?",
+  "confirmations.mute.confirm": "Utišaj",
   "confirmations.redraft.confirm": "Izbriši in preoblikuj",
-  "confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati ta status in ga preoblikovati? Vzljubi in izpostavitve bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.",
-  "confirmations.redraft.title": "Želite izbrisati in predelati objavo?",
+  "confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati to objavo in jo preoblikovati? Izkazi priljubljenosti in izpostavitve bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.",
+  "confirmations.redraft.title": "Želite izbrisati in preoblikovati objavo?",
   "confirmations.reply.confirm": "Odgovori",
   "confirmations.reply.message": "Odgovarjanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?",
   "confirmations.reply.title": "Želite prepisati objavo?",
@@ -217,7 +240,7 @@
   "conversation.with": "Z {names}",
   "copy_icon_button.copied": "Kopirano v odložišče",
   "copypaste.copied": "Kopirano",
-  "copypaste.copy_to_clipboard": "Kopiraj na odložišče",
+  "copypaste.copy_to_clipboard": "Kopiraj v odložišče",
   "directory.federated": "Iz znanega fediverzuma",
   "directory.local": "Samo iz {domain}",
   "directory.new_arrivals": "Novi prišleki",
@@ -226,28 +249,34 @@
   "disabled_account_banner.text": "Vaš račun {disabledAccount} je trenutno onemogočen.",
   "dismissable_banner.community_timeline": "To so najnovejše javne objave oseb, katerih računi gostujejo na {domain}.",
   "dismissable_banner.dismiss": "Opusti",
+  "dismissable_banner.explore_links": "Danes po fediverzumu najbolj odmevajo te novice. Višje na seznamu so novejše vesti bolj raznolikih objaviteljev.",
+  "dismissable_banner.explore_statuses": "Danes so po fediverzumu pozornost pritegnile te objave. Višje na seznamu so novejše, bolj izpostavljene in bolj priljubljene objave.",
+  "dismissable_banner.explore_tags": "Danes se po fediverzumu najpogosteje uporabljajo ti ključniki. Višje na seznamu so ključniki, ki jih uporablja več različnih ljudi.",
+  "dismissable_banner.public_timeline": "To so najnovejše javne objave ljudi s fediverzuma, ki jim sledijo ljudje na domeni {domain}.",
   "domain_block_modal.block": "Blokiraj strežnik",
   "domain_block_modal.block_account_instead": "Namesto tega blokiraj @{name}",
   "domain_block_modal.they_can_interact_with_old_posts": "Osebe s tega strežnika se lahko odzivajo na vaše stare objave.",
   "domain_block_modal.they_cant_follow": "Nihče s tega strežnika vam ne more slediti.",
   "domain_block_modal.they_wont_know": "Ne bodo vedeli, da so blokirani.",
   "domain_block_modal.title": "Blokiraj domeno?",
+  "domain_block_modal.you_will_lose_num_followers": "Izgubili boste {followersCount, plural, one {{followersCountDisplay} sledilca} two {{followersCountDisplay} sledilca} few {{followersCountDisplay} sledilce} other {{followersCountDisplay} sledilcev}} in {followingCount, plural, one {{followingCountDisplay} osebo, ki ji sledite} two {{followingCountDisplay} osebi, ki jima sledite} few {{followingCountDisplay} osebe, ki jim sledite} other {{followingCountDisplay} oseb, ki jim sledite}}.",
+  "domain_block_modal.you_will_lose_relationships": "Izgubili boste vse sledilce in ljudi, ki jim sledite na tem strežniku.",
   "domain_block_modal.you_wont_see_posts": "Objav ali obvestil uporabnikov s tega strežnika ne boste videli.",
-  "domain_pill.activitypub_lets_connect": "Omogoča vam povezovanje in interakcijo z ljudmi, ki niso samo na Mastodonu, ampak tudi na drugih družabnih platformah.",
-  "domain_pill.activitypub_like_language": "Protokol ActivityPub je kot jezik, s katerim se Mastodon pogovarja z drugimi družabnimi omrežji.",
+  "domain_pill.activitypub_lets_connect": "Omogoča vam povezovanje in interakcijo z ljudmi, ki niso samo na Mastodonu, ampak tudi na drugih družbenih platformah.",
+  "domain_pill.activitypub_like_language": "Protokol ActivityPub je kot jezik, v katerem se Mastodon pogovarja z drugimi družabnimi omrežji.",
   "domain_pill.server": "Strežnik",
-  "domain_pill.their_handle": "Njihova ročica:",
-  "domain_pill.their_server": "Njihovo digitalno domovanje, kjer bivajo vse njihove objave.",
-  "domain_pill.their_username": "Njihov edinstveni identifikator na njihovem strežniku. Uporabnike z istim uporabniškim imenom lahko najdete na različnih strežnikih.",
+  "domain_pill.their_handle": "Njegova/njena ročica:",
+  "domain_pill.their_server": "Njegovo/njeno digitalno domovanje, kjer bivajo vse njegove/njene objave.",
+  "domain_pill.their_username": "Njegov/njen edinstveni identifikator na njegovem/njenem strežniku. Uporabnike z istim uporabniškim imenom lahko najdete na različnih strežnikih.",
   "domain_pill.username": "Uporabniško ime",
   "domain_pill.whats_in_a_handle": "Kaj je v ročici?",
-  "domain_pill.who_they_are": "Ker ročice povedo, kdo je kdo in kje so, ste lahko z osebami v interakciji prek družabnega spleta <button>platform, ki jih poganja ActivityPub</button>.",
-  "domain_pill.who_you_are": "Ker ročice povedo, kdo ste in kje ste, ste lahko z osebami v interakciji prek družabnega spleta <button>platform, ki jih poganja ActivityPub</button>.",
+  "domain_pill.who_they_are": "Ker ročice povedo, kdo je kdo in kje je, lahko komunicirate z ljudmi po vsem spletu družbenih <button>platform, ki jih poganja ActivityPub</button>.",
+  "domain_pill.who_you_are": "Ker ročice povedo, kdo ste in kje ste, lahko komunicirate z ljudmi po vsem spletu družbenih <button>platform, ki jih poganja ActivityPub</button>.",
   "domain_pill.your_handle": "Vaša ročica:",
-  "domain_pill.your_server": "Vaše digitalno domovanje, kjer bivajo vse vaše objave. Vam ta ni všeč? Prenesite ga med strežniki kadar koli in z njim tudi svoje sledilce.",
+  "domain_pill.your_server": "Vaše digitalno domovanje, kjer bivajo vse vaše objave. Vam ni všeč? Kadar koli ga prenesite med strežniki in z njim tudi svoje sledilce.",
   "domain_pill.your_username": "Vaš edinstveni identifikator na tem strežniku. Uporabnike z istim uporabniškim imenom je možno najti na različnih strežnikih.",
   "embed.instructions": "Vstavite to objavo na svojo spletno stran tako, da kopirate spodnjo kodo.",
-  "embed.preview": "Tako bo izgledalo:",
+  "embed.preview": "Takole bo videti:",
   "emoji_button.activity": "Dejavnost",
   "emoji_button.clear": "Počisti",
   "emoji_button.custom": "Po meri",
@@ -263,32 +292,32 @@
   "emoji_button.search_results": "Rezultati iskanja",
   "emoji_button.symbols": "Simboli",
   "emoji_button.travel": "Potovanja in kraji",
-  "empty_column.account_hides_collections": "Ta uporabnik se je odločil, da te informacije ne bo dal na voljo",
+  "empty_column.account_hides_collections": "Ta uporabnik se je odločil, da te informacije ne bo delil",
   "empty_column.account_suspended": "Račun je suspendiran",
   "empty_column.account_timeline": "Tukaj ni objav!",
   "empty_column.account_unavailable": "Profil ni na voljo",
   "empty_column.blocks": "Niste še blokirali nobenega uporabnika.",
   "empty_column.bookmarked_statuses": "Zaenkrat še nimate zaznamovanih objav. Ko objavo zaznamujete, se pojavi tukaj.",
-  "empty_column.community": "Krajevna časovnica je prazna. Napišite nekaj javnega, da se bo snežna kepa zakotalila!",
+  "empty_column.community": "Krajevna časovnica je prazna. Napišite nekaj javnega, da se začne polniti!",
   "empty_column.direct": "Nimate še nobenih zasebnih omemb. Ko jih boste poslali ali prejeli, se bodo prikazale tukaj.",
   "empty_column.domain_blocks": "Zaenkrat ni blokiranih domen.",
-  "empty_column.explore_statuses": "Trenutno ni nič v trendu. Preverite znova kasneje!",
-  "empty_column.favourited_statuses": "Nimate priljubljenih objav. Ko boste vzljubili kakšno, bo prikazana tukaj.",
-  "empty_column.favourites": "Nihče še ni vzljubil te objave. Ko jo bo nekdo, se bo pojavila tukaj.",
+  "empty_column.explore_statuses": "Trenutno ni novih trendov. Preverite znova kasneje!",
+  "empty_column.favourited_statuses": "Nimate priljubljenih objav. Ko boste kakšno dodali med priljubljene, bo prikazana tukaj.",
+  "empty_column.favourites": "Nihče še ni vzljubil te objave. Ko jo bo nekdo, bo naveden tukaj.",
   "empty_column.follow_requests": "Nimate prošenj za sledenje. Ko boste prejeli kakšno, se bo prikazala tukaj.",
-  "empty_column.followed_tags": "Zaenkrat ne sledite še nobenemu ključniku. Ko boste, se bodo pojavili tukaj.",
-  "empty_column.hashtag": "V tem ključniku še ni nič.",
-  "empty_column.home": "Vaša domača časovnica je prazna! Sledite več osebam, da jo zapolnite. {suggestions}",
-  "empty_column.list": "Na tem seznamu ni ničesar. Ko bodo člani tega seznama objavili nove statuse, se bodo pojavili tukaj.",
+  "empty_column.followed_tags": "Zaenkrat ne sledite še nobenemu ključniku. Ko boste, se bo pojavil tukaj.",
+  "empty_column.hashtag": "V tem ključniku ni še nič.",
+  "empty_column.home": "Vaša domača časovnica je prazna! Sledite več osebam, da jo zapolnite.",
+  "empty_column.list": "Na tem seznamu ni ničesar. Ko bodo člani tega seznama kaj objavili, se bodo te objave pojavile tukaj.",
   "empty_column.mutes": "Niste utišali še nobenega uporabnika.",
   "empty_column.notification_requests": "Vse prebrano! Tu ni ničesar več. Ko prejmete nova obvestila, se bodo pojavila tu glede na vaše nastavitve.",
   "empty_column.notifications": "Nimate še nobenih obvestil. Povežite se z drugimi, da začnete pogovor.",
-  "empty_column.public": "Tukaj ni ničesar! Da ga napolnite, napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih strežnikov",
+  "empty_column.public": "Tukaj ni ničesar! Napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih strežnikov, da se bo napolnilo",
   "error.unexpected_crash.explanation": "Zaradi hrošča v naši kodi ali težave z združljivostjo brskalnika te strani ni mogoče ustrezno prikazati.",
-  "error.unexpected_crash.explanation_addons": "Te strani ni mogoče ustrezno prikazati. To napako najverjetneje povzroča dodatek briskalnika ali samodejna orodja za prevajanje.",
+  "error.unexpected_crash.explanation_addons": "Te strani ni mogoče ustrezno prikazati. To napako najverjetneje povzroča dodatek brskalnika ali samodejna orodja za prevajanje.",
   "error.unexpected_crash.next_steps": "Poskusite osvežiti stran. Če to ne pomaga, boste morda še vedno lahko uporabljali Mastodon prek drugega brskalnika ali z domorodno aplikacijo.",
   "error.unexpected_crash.next_steps_addons": "Poskusite jih onemogočiti in osvežiti stran. Če to ne pomaga, boste morda še vedno lahko uporabljali Mastodon prek drugega brskalnika ali z domorodno aplikacijo.",
-  "errors.unexpected_crash.copy_stacktrace": "Kopiraj sledenje skladu na odložišče",
+  "errors.unexpected_crash.copy_stacktrace": "Kopiraj sled sklada na odložišče",
   "errors.unexpected_crash.report_issue": "Prijavi težavo",
   "explore.suggested_follows": "Ljudje",
   "explore.title": "Razišči",
@@ -297,7 +326,7 @@
   "explore.trending_tags": "Ključniki",
   "filter_modal.added.context_mismatch_explanation": "Ta kategorija filtra ne velja za kontekst, v katerem ste dostopali do te objave. Če želite, da je objava filtrirana tudi v tem kontekstu, morate urediti filter.",
   "filter_modal.added.context_mismatch_title": "Neujemanje konteksta!",
-  "filter_modal.added.expired_explanation": "Ta kategorija filtra je pretekla, morali boste spremeniti datum veljavnosti, da bo veljal še naprej.",
+  "filter_modal.added.expired_explanation": "Ta kategorija filtra je pretekla. Morali boste spremeniti datum veljavnosti, da bo veljal še naprej.",
   "filter_modal.added.expired_title": "Filter je pretekel!",
   "filter_modal.added.review_and_configure": "Če želite pregledati in nadalje prilagoditi kategorijo filtra, obiščite {settings_link}.",
   "filter_modal.added.review_and_configure_title": "Nastavitve filtra",
@@ -312,6 +341,7 @@
   "filter_modal.select_filter.title": "Filtriraj to objavo",
   "filter_modal.title.status": "Filtrirajte objavo",
   "filter_warning.matches_filter": "Se ujema s filtrom »<span>{title}</span>«",
+  "filtered_notifications_banner.pending_requests": "Od {count, plural, =0 {nikogar, ki bi ga poznali} one {nekoga, ki ga morda poznate} two {dveh ljudi, ki ju morda poznate} other {ljudi, ki jih morda poznate}}",
   "filtered_notifications_banner.title": "Filtrirana obvestila",
   "firehose.all": "Vse",
   "firehose.local": "Ta strežnik",
@@ -360,9 +390,12 @@
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objav} other {{counter} objav}}",
   "hashtag.follow": "Sledi ključniku",
   "hashtag.unfollow": "Nehaj slediti ključniku",
-  "hashtags.and_other": "…in še {count, plural, other {#}}",
+  "hashtags.and_other": "… in še {count, plural, other {#}}",
+  "hints.profiles.followers_may_be_missing": "Sledilci za ta profil morda manjkajo.",
+  "hints.profiles.follows_may_be_missing": "Osebe, ki jim ta profil sledi, morda manjkajo.",
   "hints.profiles.posts_may_be_missing": "Nekatere objave s tega profila morda manjkajo.",
   "hints.profiles.see_more_followers": "Pokaži več sledilcev na {domain}",
+  "hints.profiles.see_more_follows": "Pokaži več sledenih ljudi na zbirališču {domain}",
   "hints.profiles.see_more_posts": "Pokaži več objav na {domain}",
   "hints.threads.replies_may_be_missing": "Odgovori z drugih strežnikov morda manjkajo.",
   "hints.threads.see_more": "Pokaži več odgovorov na {domain}",
@@ -373,9 +406,25 @@
   "home.pending_critical_update.link": "Glejte posodobitve",
   "home.pending_critical_update.title": "Na voljo je kritična varnostna posodobbitev!",
   "home.show_announcements": "Pokaži obvestila",
+  "ignore_notifications_modal.disclaimer": "Mastodon ne more obveščati uporabnikov, da ste prezrli njihova obvestila. Tudi če jih prezrete, jih lahko uporabniki še vedno pošiljajo.",
+  "ignore_notifications_modal.filter_instead": "Raje filtriraj",
+  "ignore_notifications_modal.filter_to_act_users": "Še vedno boste lahko sprejeli, zavrnili ali prijavili uporabnike",
   "ignore_notifications_modal.filter_to_avoid_confusion": "Filtriranje pomaga pri izogibanju morebitni zmedi",
   "ignore_notifications_modal.filter_to_review_separately": "Filtrirana obvestila lahko pregledate ločeno",
   "ignore_notifications_modal.ignore": "Prezri obvestila",
+  "ignore_notifications_modal.limited_accounts_title": "Naj prezrem obvestila moderiranih računov?",
+  "ignore_notifications_modal.new_accounts_title": "Naj prezrem obvestila novih računov?",
+  "ignore_notifications_modal.not_followers_title": "Naj prezrem obvestila ljudi, ki vam ne sledijo?",
+  "ignore_notifications_modal.not_following_title": "Naj prezrem obvestila ljudi, ki jim ne sledite?",
+  "ignore_notifications_modal.private_mentions_title": "Naj prezrem obvestila od nezaželenih zasebnih omemb?",
+  "info_button.label": "Pomoč",
+  "info_button.what_is_alt_text": "<h1>Kaj je nadomestno besedilo?</h1> <p>Z nadomestnim besedilom dodatno opišemo sliko in tako pomagamo slabovidnim, ljudem s slabo internetno povezavo in tistim, ki jim manjka kontekst.</p> <p>Vaša objava bo veliko bolj dostopna in razumljiva, če boste napisali jasno, jedrnato in nepristransko nadomestno besedilo.</p> <ul> <li>Izpostavite pomembne elemente.</li> <li>Povzemite besedilo v slikah.</li> <li>Pišite v celih stavkih.</li> <li>Zajemite bistvo, ne dolgovezite.</li> <li>Opišite težnje in ključna odkritja, ki ste jih razbrali iz zapletenih grafik (npr. diagramov ali zemljevidov).</li> </ul>",
+  "interaction_modal.action.favourite": "Med priljubljene lahko dodate, ko se vpišete v svoj račun.",
+  "interaction_modal.action.follow": "Sledite lahko šele, ko se vpišete v svoj račun.",
+  "interaction_modal.action.reblog": "Izpostavite lahko šele, ko se vpišete v svoj račun.",
+  "interaction_modal.action.reply": "Odgovorite lahko šele, ko se vpišete v svoj račun.",
+  "interaction_modal.action.vote": "Glasujete lahko šele, ko se vpišete v svoj račun.",
+  "interaction_modal.go": "Naprej",
   "interaction_modal.no_account_yet": "Še nimate računa?",
   "interaction_modal.on_another_server": "Na drugem strežniku",
   "interaction_modal.on_this_server": "Na tem strežniku",
@@ -383,6 +432,8 @@
   "interaction_modal.title.follow": "Sledi {name}",
   "interaction_modal.title.reblog": "Izpostavi objavo {name}",
   "interaction_modal.title.reply": "Odgovori na objavo {name}",
+  "interaction_modal.title.vote": "Izpolni anketo uporabnika/ce {name}",
+  "interaction_modal.username_prompt": "Npr. {example}",
   "intervals.full.days": "{number, plural, one {# dan} two {# dni} few {# dni} other {# dni}}",
   "intervals.full.hours": "{number, plural, one {# ura} two {# uri} few {# ure} other {# ur}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}",
@@ -407,7 +458,7 @@
   "keyboard_shortcuts.muted": "Odpri seznam utišanih uporabnikov",
   "keyboard_shortcuts.my_profile": "Odprite svoj profil",
   "keyboard_shortcuts.notifications": "Odpri stolpec z obvestili",
-  "keyboard_shortcuts.open_media": "Odpri medij",
+  "keyboard_shortcuts.open_media": "Odpri predstavnost",
   "keyboard_shortcuts.pinned": "Odpri seznam pripetih objav",
   "keyboard_shortcuts.profile": "Odpri avtorjev profil",
   "keyboard_shortcuts.reply": "Odgovori na objavo",
@@ -416,26 +467,35 @@
   "keyboard_shortcuts.spoilers": "Pokaži/skrij polje CW",
   "keyboard_shortcuts.start": "Odpri stolpec \"začni\"",
   "keyboard_shortcuts.toggle_hidden": "Pokaži/skrij besedilo za CW",
-  "keyboard_shortcuts.toggle_sensitivity": "Pokaži/skrij medije",
+  "keyboard_shortcuts.toggle_sensitivity": "Pokaži/skrij predstavnosti",
   "keyboard_shortcuts.toot": "Začni povsem novo objavo",
+  "keyboard_shortcuts.translate": "za prevod objave",
   "keyboard_shortcuts.unfocus": "Odstrani pozornost z območja za sestavljanje besedila/iskanje",
   "keyboard_shortcuts.up": "Premakni navzgor po seznamu",
   "lightbox.close": "Zapri",
   "lightbox.next": "Naslednji",
   "lightbox.previous": "Prejšnji",
+  "lightbox.zoom_in": "Približaj na dejansko velikost",
+  "lightbox.zoom_out": "Čez cel prikaz",
   "limited_account_hint.action": "Vseeno pokaži profil",
   "limited_account_hint.title": "Profil so moderatorji strežnika {domain} skrili.",
-  "link_preview.author": "Avtor_ica {name}",
+  "link_preview.author": "Avtor/ica {name}",
   "link_preview.more_from_author": "Več od {name}",
   "link_preview.shares": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}",
   "lists.add_member": "Dodaj",
   "lists.add_to_list": "Dodaj na seznam",
+  "lists.add_to_lists": "Dodaj {name} na sezname",
   "lists.create": "Ustvari",
+  "lists.create_a_list_to_organize": "Uredite si domači vir z novim seznamom",
   "lists.create_list": "Ustvari seznam",
   "lists.delete": "Izbriši seznam",
   "lists.done": "Opravljeno",
   "lists.edit": "Uredi seznam",
+  "lists.exclusive": "Skrij člane v domovanju",
+  "lists.exclusive_hint": "Objave vseh, ki so na tem seznamu, se ne pokažejo v vašem domačem viru. Tako se izognete podvojenim objavam.",
   "lists.find_users_to_add": "Poišči člane za dodajanje",
+  "lists.list_members": "Člani seznama",
+  "lists.list_members_count": "{count, plural, one {# član} two {# člana} few {# člani} other {# članov}}",
   "lists.list_name": "Ime seznama",
   "lists.new_list_name": "Novo ime seznama",
   "lists.no_lists_yet": "Ni seznamov.",
@@ -446,6 +506,8 @@
   "lists.replies_policy.list": "Članom seznama",
   "lists.replies_policy.none": "Nikomur",
   "lists.save": "Shrani",
+  "lists.search": "Iskanje",
+  "lists.show_replies_to": "Vključi odgovore, katerih pošiljatelji so člani seznama in prejemniki",
   "load_pending": "{count, plural, one {# nov element} two {# nova elementa} few {# novi elementi} other {# novih elementov}}",
   "loading_indicator.label": "Nalaganje …",
   "media_gallery.hide": "Skrij",
@@ -493,9 +555,17 @@
   "notification.admin.report_statuses": "{name} je prijavil/a {target} zaradi {category}",
   "notification.admin.report_statuses_other": "{name} je prijavil/a {target}",
   "notification.admin.sign_up": "{name} se je vpisal/a",
+  "notification.admin.sign_up.name_and_others": "Prijavili so se {name} in {count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}}",
+  "notification.annual_report.message": "Čaka vas vaš #Wrapstodon {year}! Razkrijte svoje letošnje nepozabne trenutke na Mastodonu!",
+  "notification.annual_report.view": "Pokaži #Wrapstodon",
   "notification.favourite": "{name} je vzljubil/a vašo objavo",
+  "notification.favourite.name_and_others_with_link": "{name} in <a>{count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}}</a> je dodalo vašo objavo med priljubljene",
+  "notification.favourite_pm": "{name} je dodalo vašo zasebno omembo med priljubljene",
+  "notification.favourite_pm.name_and_others_with_link": "{name} in <a>{count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}}</a> je dodalo vašo zasebno omembo med priljubljene",
   "notification.follow": "{name} vam sledi",
+  "notification.follow.name_and_others": "{name} in {count, plural, one {<a># druga oseba</a> sta ti sledila} two {<a># drugi osebi</a> so ti sledili} few {<a># druge osebe</a> so ti sledili} other {<a># drugih oseb</a> ti je sledilo}}",
   "notification.follow_request": "{name} vam želi slediti",
+  "notification.follow_request.name_and_others": "{name} in {count, plural, one {# druga oseba bi ti rada sledila} two {# drugi osebi bi ti radi sledili} few {# druge osebe bi ti radi sledili} other {# drugih oseb bi ti radi sledili}}",
   "notification.label.mention": "Omemba",
   "notification.label.private_mention": "Zasebna omemba",
   "notification.label.private_reply": "Zasebni odgovor",
@@ -514,6 +584,7 @@
   "notification.own_poll": "Vaša anketa je zaključena",
   "notification.poll": "Anketa, v kateri ste sodelovali, je zaključena",
   "notification.reblog": "{name} je izpostavila/a vašo objavo",
+  "notification.reblog.name_and_others_with_link": "{name} in <a>{count, plural, one {# druga oseba</a> sta izpostavila tvojo objavo} two {# drugi osebi</a> so izpostavili tvojo objavo} few {# druge osebe</a> so izpostavili tvojo objavo} other {# drugih oseb</a> so izpostavili tvojo objavo}}",
   "notification.relationships_severance_event": "Povezave z {name} prekinjene",
   "notification.relationships_severance_event.account_suspension": "Skrbnik na {from} je suspendiral račun {target}, kar pomeni, da od računa ne morete več prejemati posodobitev ali imeti z njim interakcij.",
   "notification.relationships_severance_event.domain_block": "Skrbnik na {from} je blokiral domeno {target}, vključno z vašimi sledilci ({followersCount}) in {followingCount, plural, one {# računom, ki mu sledite} two {# računoma, ki jima sledite} few {# računi, ki jim sledite} other {# računi, ki jim sledite}}.",
@@ -522,12 +593,21 @@
   "notification.status": "{name} je pravkar objavil/a",
   "notification.update": "{name} je uredil(a) objavo",
   "notification_requests.accept": "Sprejmi",
+  "notification_requests.accept_multiple": "{count, plural, one {Sprejmi # prošnjo …} two {Sprejmi # prošnji …} few {Sprejmi # prošnje …} other {Sprejmi # prošenj …}}",
+  "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Sprejmi prošnjo} two {Sprejmi prošnji} other {Sprejmi prošnje}}",
+  "notification_requests.confirm_accept_multiple.message": "Sprejeti nameravate {count, plural, one {eno prošnjo za obvestila} two {dve prošnji za obvestila} few {# prošnje za obvestila} other {# prošenj za obvestila}}. Ali ste prepričani?",
   "notification_requests.confirm_accept_multiple.title": "Ali želite sprejeti zahteve za obvestila?",
+  "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Zavrni prošnjo} two {Zavrni prošnji} other {Zavrni prošnje}}",
+  "notification_requests.confirm_dismiss_multiple.message": "Zavrniti nameravate {count, plural, one {eno prošnjo za obvestila} two {dve prošnji za obvestila} few {# prošnje za obvestila} other {# prošenj za obvestila}}. Do {count, plural, one {nje} two {njiju} other {njih}} ne boste več mogli dostopati. Ali ste prepričani?",
   "notification_requests.confirm_dismiss_multiple.title": "Želite opustiti zahteve za obvestila?",
   "notification_requests.dismiss": "Zavrni",
+  "notification_requests.dismiss_multiple": "{count, plural, one {Zavrni # prošnjo …} two {Zavrni # prošnji …} few {Zavrni # prošnje …} other {Zavrni # prošenj …}}",
   "notification_requests.edit_selection": "Uredi",
   "notification_requests.exit_selection": "Opravljeno",
+  "notification_requests.explainer_for_limited_account": "Obvestila za ta račun so bila filtrirana, ker je ta račun omejil moderator.",
+  "notification_requests.explainer_for_limited_remote_account": "Obvestila za ta račun so bila filtrirana, ker je račun ali njegov strežnik omejil moderator.",
   "notification_requests.maximize": "Maksimiraj",
+  "notification_requests.minimize_banner": "Zloži pasico filtriranih obvestil",
   "notification_requests.notifications_from": "Obvestila od {name}",
   "notification_requests.title": "Filtrirana obvestila",
   "notification_requests.view": "Pokaži obvestila",
@@ -542,6 +622,7 @@
   "notifications.column_settings.filter_bar.category": "Vrstica za hitro filtriranje",
   "notifications.column_settings.follow": "Novi sledilci:",
   "notifications.column_settings.follow_request": "Nove prošnje za sledenje:",
+  "notifications.column_settings.group": "Združi",
   "notifications.column_settings.mention": "Omembe:",
   "notifications.column_settings.poll": "Rezultati ankete:",
   "notifications.column_settings.push": "Potisna obvestila",
@@ -568,6 +649,9 @@
   "notifications.policy.accept": "Sprejmi",
   "notifications.policy.accept_hint": "Pokaži med obvestili",
   "notifications.policy.drop": "Prezri",
+  "notifications.policy.drop_hint": "Pošlji v pozabo, od koder se nikdar nič ne vrne",
+  "notifications.policy.filter": "Filtriraj",
+  "notifications.policy.filter_hint": "Pošlji med filtrirana prejeta obvestila",
   "notifications.policy.filter_limited_accounts_hint": "Omejeno s strani moderatorjev strežnika",
   "notifications.policy.filter_limited_accounts_title": "Moderirani računi",
   "notifications.policy.filter_new_accounts.hint": "Ustvarjen v {days, plural, one {zadnjem # dnevu} two {zadnjih # dnevih} few {zadnjih # dnevih} other {zadnjih # dnevih}}",
@@ -585,12 +669,14 @@
   "onboarding.follows.back": "Nazaj",
   "onboarding.follows.done": "Opravljeno",
   "onboarding.follows.empty": "Žal trenutno ni mogoče prikazati nobenih rezultatov. Lahko poskusite z iskanjem ali brskanjem po strani za raziskovanje, da poiščete osebe, ki jim želite slediti, ali poskusite znova pozneje.",
+  "onboarding.follows.search": "Išči",
+  "onboarding.follows.title": "Vaš prvi korak je, da sledite ljudem",
   "onboarding.profile.discoverable": "Naj bo moj profil mogoče najti",
   "onboarding.profile.discoverable_hint": "Ko se odločite za razkrivanje na Mastodonu, se lahko vaše objave pojavijo v rezultatih iskanja in trendih, vaš profil pa se lahko predlaga ljudem, ki imajo podobne interese kot vi.",
   "onboarding.profile.display_name": "Pojavno ime",
   "onboarding.profile.display_name_hint": "Vaše polno ime ali lažno ime ...",
   "onboarding.profile.note": "Biografija",
-  "onboarding.profile.note_hint": "Druge osebe lahko @omenite ali #ključite ...",
+  "onboarding.profile.note_hint": "Lahko @omenite druge osebe ali dodate #ključnike ...",
   "onboarding.profile.save_and_continue": "Shrani in nadaljuj",
   "onboarding.profile.title": "Nastavitev profila",
   "onboarding.profile.upload_avatar": "Naloži sliko profila",
@@ -610,7 +696,7 @@
   "poll_button.remove_poll": "Odstrani anketo",
   "privacy.change": "Spremeni zasebnost objave",
   "privacy.direct.long": "Vsem omenjenim v objavi",
-  "privacy.direct.short": "Določenim ljudem",
+  "privacy.direct.short": "Zasebna omemba",
   "privacy.private.long": "Samo vašim sledilcem",
   "privacy.private.short": "Sledilcem",
   "privacy.public.long": "Vsem, ki so ali niso na Mastodonu",
@@ -622,6 +708,8 @@
   "privacy_policy.title": "Pravilnik o zasebnosti",
   "recommended": "Priporočeno",
   "refresh": "Osveži",
+  "regeneration_indicator.please_stand_by": "Prosimo, počakajte.",
+  "regeneration_indicator.preparing_your_home_feed": "Pripravljamo vaš domači vir …",
   "relative_time.days": "{number} d",
   "relative_time.full.days": "{number, plural, one {pred # dnem} two {pred # dnevoma} few {pred # dnevi} other {pred # dnevi}}",
   "relative_time.full.hours": "{number, plural, one {pred # uro} two {pred # urama} few {pred # urami} other {pred # urami}}",
@@ -657,7 +745,7 @@
   "report.reasons.dislike": "Ni mi všeč",
   "report.reasons.dislike_description": "To ni tisto, kar želim videti",
   "report.reasons.legal": "To ni legalno",
-  "report.reasons.legal_description": "Ste mnenja, da krši zakonodajo vaše države ali države strežnika",
+  "report.reasons.legal_description": "Sem mnenja, da krši zakonodajo moje države ali države strežnika",
   "report.reasons.other": "Gre za nekaj drugega",
   "report.reasons.other_description": "Težava ne sodi v druge kategorije",
   "report.reasons.spam": "To je neželena vsebina",
@@ -669,10 +757,10 @@
   "report.statuses.subtitle": "Izberite vse, kar ustreza",
   "report.statuses.title": "Ali so kakšne objave, ki dokazujejo trditve iz te prijave?",
   "report.submit": "Pošlji",
-  "report.target": "Prijavi {target}",
+  "report.target": "Prijavljate {target}",
   "report.thanks.take_action": "Tukaj so vaše možnosti za nadzor tistega, kar vidite na Mastodonu:",
   "report.thanks.take_action_actionable": "Medtem, ko to pregledujemo, lahko proti @{name} ukrepate:",
-  "report.thanks.title": "Ali ne želite tega videti?",
+  "report.thanks.title": "Ali ne želite videti tega?",
   "report.thanks.title_actionable": "Hvala za prijavo, bomo preverili.",
   "report.unfollow": "Ne sledi več @{name}",
   "report.unfollow_explanation": "Temu računu sledite. Da ne boste več videli njegovih objav v svojem domačem viru, mu prenehajte slediti.",
@@ -696,7 +784,7 @@
   "search.search_or_paste": "Iščite ali prilepite URL",
   "search_popout.full_text_search_disabled_message": "Ni dostopno na {domain}.",
   "search_popout.full_text_search_logged_out_message": "Na voljo le, če ste prijavljeni.",
-  "search_popout.language_code": "Koda ISO jezika",
+  "search_popout.language_code": "Jezikovna koda ISO",
   "search_popout.options": "Možnosti iskanja",
   "search_popout.quick_actions": "Hitra dejanja",
   "search_popout.recent": "Nedavna iskanja",
@@ -706,8 +794,10 @@
   "search_results.all": "Vse",
   "search_results.hashtags": "Ključniki",
   "search_results.no_results": "Ni rezultatov.",
+  "search_results.no_search_yet": "Pobrskajte med objavami, profili in ključniki.",
   "search_results.see_all": "Poglej vse",
   "search_results.statuses": "Objave",
+  "search_results.title": "Zadetki za \"{q}\"",
   "server_banner.about_active_users": "Osebe, ki so uporabljale ta strežnik zadnjih 30 dni (dejavni uporabniki meseca)",
   "server_banner.active_users": "dejavnih uporabnikov",
   "server_banner.administered_by": "Upravlja:",
@@ -725,6 +815,7 @@
   "status.bookmark": "Dodaj med zaznamke",
   "status.cancel_reblog_private": "Prekliči izpostavitev",
   "status.cannot_reblog": "Te objave ni mogoče izpostaviti",
+  "status.continued_thread": "Nadaljevanje niti",
   "status.copy": "Kopiraj povezavo do objave",
   "status.delete": "Izbriši",
   "status.detailed_status": "Podroben pogled pogovora",
@@ -734,7 +825,7 @@
   "status.edited": "Zadnje urejanje {date}",
   "status.edited_x_times": "Urejeno {count, plural, one {#-krat} two {#-krat} few {#-krat} other {#-krat}}",
   "status.embed": "Pridobite kodo za vgradnjo",
-  "status.favourite": "Priljubljen_a",
+  "status.favourite": "Priljubljen/a",
   "status.favourites": "{count, plural, one {priljubitev} two {priljubitvi} few {priljubitve} other {priljubitev}}",
   "status.filter": "Filtriraj to objavo",
   "status.history.created": "{name}: ustvarjeno {date}",
@@ -742,7 +833,7 @@
   "status.load_more": "Naloži več",
   "status.media.open": "Kliknite za odpiranje",
   "status.media.show": "Kliknite za prikaz",
-  "status.media_hidden": "Mediji so skriti",
+  "status.media_hidden": "Predstavnosti so skrite",
   "status.mention": "Omeni @{name}",
   "status.more": "Več",
   "status.mute": "Utišaj @{name}",
@@ -759,6 +850,7 @@
   "status.redraft": "Izbriši in preoblikuj",
   "status.remove_bookmark": "Odstrani zaznamek",
   "status.remove_favourite": "Odstrani iz priljubljenih",
+  "status.replied_in_thread": "Odgovor iz niti",
   "status.replied_to": "Odgovoril/a {name}",
   "status.reply": "Odgovori",
   "status.replyAll": "Odgovori na nit",
@@ -768,7 +860,7 @@
   "status.show_less_all": "Prikaži manj za vse",
   "status.show_more_all": "Pokaži več za vse",
   "status.show_original": "Pokaži izvirnik",
-  "status.title.with_attachments": "{user} je objavil_a {attachmentCount, plural, one {{attachmentCount} priponko} two {{attachmentCount} priponki} few {{attachmentCount} priponke} other {{attachmentCount} priponk}}",
+  "status.title.with_attachments": "{user} je objavil/a {attachmentCount, plural, one {{attachmentCount} priponko} two {{attachmentCount} priponki} few {{attachmentCount} priponke} other {{attachmentCount} priponk}}",
   "status.translate": "Prevedi",
   "status.translated_from_with": "Prevedeno iz {lang} s pomočjo {provider}",
   "status.uncached_media_warning": "Predogled ni na voljo",
@@ -779,7 +871,9 @@
   "subscribed_languages.target": "Spremeni naročene jezike za {target}",
   "tabs_bar.home": "Domov",
   "tabs_bar.notifications": "Obvestila",
+  "terms_of_service.effective_as_of": "Veljavno od {date}",
   "terms_of_service.title": "Pogoji uporabe",
+  "terms_of_service.upcoming_changes_on": "Spremembe začnejo veljati {date}",
   "time_remaining.days": "{number, plural, one {preostaja # dan} two {preostajata # dneva} few {preostajajo # dnevi} other {preostaja # dni}}",
   "time_remaining.hours": "{number, plural, one {# ura} other {# ur}} je ostalo",
   "time_remaining.minutes": "{number, plural, one {# minuta} other {# minut}} je ostalo",
@@ -795,6 +889,11 @@
   "upload_button.label": "Dodajte slike, video ali zvočno datoteko",
   "upload_error.limit": "Omejitev prenosa datoteke je presežena.",
   "upload_error.poll": "Prenos datoteke z anketami ni dovoljen.",
+  "upload_form.drag_and_drop.instructions": "Predstavnostno priponko lahko poberete tako, da pritisnete preslednico ali vnašalko. S puščicami na tipkovnici premikate priponko v posamezno smer. Priponko lahko odložite na novem položaju s ponovnim pritiskom na preslednico ali vnašalko ali pa dejanje prekličete s tipko ubežnica.",
+  "upload_form.drag_and_drop.on_drag_cancel": "Premikanje priponke je preklicano. Predstavnostna priponka {item} je padla nazaj na prejšnje mesto.",
+  "upload_form.drag_and_drop.on_drag_end": "Predstavnostna priponka {item} je padla nazaj.",
+  "upload_form.drag_and_drop.on_drag_over": "Priponka {item} je bila premaknjena.",
+  "upload_form.drag_and_drop.on_drag_start": "Pobrana priponka {item}.",
   "upload_form.edit": "Uredi",
   "upload_progress.label": "Pošiljanje ...",
   "upload_progress.processing": "Obdelovanje …",
@@ -805,8 +904,6 @@
   "video.expand": "Razširi video",
   "video.fullscreen": "Celozaslonski način",
   "video.hide": "Skrij video",
-  "video.mute": "Utišaj zvok",
   "video.pause": "Premor",
-  "video.play": "Predvajaj",
-  "video.unmute": "Vklopi zvok"
+  "video.play": "Predvajaj"
 }
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 29b8a8263a..a42a25b374 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Përpunoni profilin",
   "account.enable_notifications": "Njoftomë, kur poston @{name}",
   "account.endorse": "Pasqyrojeni në profil",
+  "account.featured": "Të zgjedhur",
+  "account.featured.hashtags": "Hashtag-ë",
+  "account.featured.posts": "Postime",
   "account.featured_tags.last_status_at": "Postimi i fundit më {date}",
   "account.featured_tags.last_status_never": "Pa postime",
-  "account.featured_tags.title": "Hashtagë të zgjedhur të {name}",
   "account.follow": "Ndiqeni",
   "account.follow_back": "Ndiqe gjithashtu",
   "account.followers": "Ndjekës",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} postim} other {{counter} postime}}",
   "account.unblock": "Zhbllokoje @{name}",
   "account.unblock_domain": "Zhblloko përkatësinë {domain}",
+  "account.unblock_domain_short": "Zhbllokoje",
   "account.unblock_short": "Zhbllokoje",
   "account.unendorse": "Mos e përfshi në profil",
   "account.unfollow": "Resht së ndjekuri",
@@ -288,6 +291,7 @@
   "emoji_button.search_results": "Përfundime kërkimi",
   "emoji_button.symbols": "Simbole",
   "emoji_button.travel": "Udhëtime & Vende",
+  "empty_column.account_featured": "Kjo listë është e zbrazët",
   "empty_column.account_hides_collections": "Ky përdorues ka zgjedhur të mos e japë këtë informacion",
   "empty_column.account_suspended": "Llogaria u pezullua",
   "empty_column.account_timeline": "S’ka mesazhe këtu!",
@@ -372,6 +376,8 @@
   "generic.saved": "U ruajt",
   "getting_started.heading": "Si t’ia fillohet",
   "hashtag.admin_moderation": "Hap ndërfaqe moderimi për #{name}",
+  "hashtag.browse": "Shfletoni postime me #{hashtag}",
+  "hashtag.browse_from_account": "Shfletoni postime nga @{name} me #{hashtag}",
   "hashtag.column_header.tag_mode.all": "dhe {additional}",
   "hashtag.column_header.tag_mode.any": "ose {additional}",
   "hashtag.column_header.tag_mode.none": "pa {additional}",
@@ -385,6 +391,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} postim} other {{counter} postime}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} postim} other {{counter} postime}} sot",
   "hashtag.follow": "Ndiqe hashtag-un",
+  "hashtag.mute": "Heshtoje #{hashtag}",
   "hashtag.unfollow": "Hiqe ndjekjen e hashtag-ut",
   "hashtags.and_other": "…dhe {count, plural, one {}other {# më tepër}}",
   "hints.profiles.followers_may_be_missing": "Mund të mungojnë ndjekës për këtë profil.",
@@ -692,7 +699,7 @@
   "poll_button.remove_poll": "Hiqe pyetësorin",
   "privacy.change": "Rregulloni privatësi mesazhesh",
   "privacy.direct.long": "Gjithkënd i përmendur te postimi",
-  "privacy.direct.short": "Persona të veçantë",
+  "privacy.direct.short": "Përmendje private",
   "privacy.private.long": "Vetëm ndjekësit tuaj",
   "privacy.private.short": "Ndjekës",
   "privacy.public.long": "Cilido që hyn e del në Mastodon",
@@ -867,7 +874,9 @@
   "subscribed_languages.target": "Ndryshoni gjuhë pajtimesh për {target}",
   "tabs_bar.home": "Kreu",
   "tabs_bar.notifications": "Njoftime",
+  "terms_of_service.effective_as_of": "Hyn në fuqi që prej {date}",
   "terms_of_service.title": "Kushte Shërbimi",
+  "terms_of_service.upcoming_changes_on": "Ndryshime të ardhshme më {date}",
   "time_remaining.days": "Edhe {number, plural, one {# ditë} other {# ditë}}",
   "time_remaining.hours": "Edhe {number, plural, one {# orë} other {# orë}}",
   "time_remaining.minutes": "Edhe {number, plural, one {# minutë} other {# minuta}}",
@@ -898,8 +907,12 @@
   "video.expand": "Zgjeroje videon",
   "video.fullscreen": "Sa krejt ekrani",
   "video.hide": "Fshihe videon",
-  "video.mute": "Hiqi zërin",
+  "video.mute": "Heshtoje",
   "video.pause": "Ndalesë",
   "video.play": "Luaje",
-  "video.unmute": "Riktheji zërin"
+  "video.skip_backward": "Anashkalo pararendësen",
+  "video.skip_forward": "Anashkalo pasardhësen",
+  "video.unmute": "Hiqi heshtimin",
+  "video.volume_down": "Ulje volumi",
+  "video.volume_up": "Ngritje volumi"
 }
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 36a7ed760b..2d00533e0e 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -28,7 +28,6 @@
   "account.endorse": "Istakni na profilu",
   "account.featured_tags.last_status_at": "Poslednja objava {date}",
   "account.featured_tags.last_status_never": "Nema objava",
-  "account.featured_tags.title": "Istaknute heš oznake korisnika {name}",
   "account.follow": "Prati",
   "account.follow_back": "Uzvrati praćenje",
   "account.followers": "Pratioci",
@@ -527,7 +526,6 @@
   "poll_button.remove_poll": "Ukloni anketu",
   "privacy.change": "Promeni privatnost objave",
   "privacy.direct.long": "Svi pomenuti u objavi",
-  "privacy.direct.short": "Određeni ljudi",
   "privacy.private.long": "Samo vaši pratioci",
   "privacy.private.short": "Pratioci",
   "privacy.public.long": "Bilo ko na Mastodon-u i van njega",
@@ -714,8 +712,6 @@
   "video.expand": "Proširi video",
   "video.fullscreen": "Ceo ekran",
   "video.hide": "Sakrij video",
-  "video.mute": "Isključi zvuk",
   "video.pause": "Pauziraj",
-  "video.play": "Reprodukuj",
-  "video.unmute": "Uključi zvuk"
+  "video.play": "Reprodukuj"
 }
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index f248a29da0..af323bed27 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -28,7 +28,6 @@
   "account.endorse": "Истакни на профилу",
   "account.featured_tags.last_status_at": "Последња објава {date}",
   "account.featured_tags.last_status_never": "Нема објава",
-  "account.featured_tags.title": "Истакнуте хеш ознаке корисника {name}",
   "account.follow": "Прати",
   "account.follow_back": "Узврати праћење",
   "account.followers": "Пратиоци",
@@ -527,7 +526,6 @@
   "poll_button.remove_poll": "Уклони анкету",
   "privacy.change": "Промени приватност објаве",
   "privacy.direct.long": "Сви поменути у објави",
-  "privacy.direct.short": "Одређени људи",
   "privacy.private.long": "Само ваши пратиоци",
   "privacy.private.short": "Пратиоци",
   "privacy.public.long": "Било ко на Mastodon-у и ван њега",
@@ -714,8 +712,6 @@
   "video.expand": "Прошири видео",
   "video.fullscreen": "Цео екран",
   "video.hide": "Сакриј видео",
-  "video.mute": "Искључи звук",
   "video.pause": "Паузирај",
-  "video.play": "Репродукуј",
-  "video.unmute": "Укључи звук"
+  "video.play": "Репродукуј"
 }
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index da6a49b75a..c23a645135 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -29,7 +29,6 @@
   "account.endorse": "Visa på profil",
   "account.featured_tags.last_status_at": "Senaste inlägg den {date}",
   "account.featured_tags.last_status_never": "Inga inlägg",
-  "account.featured_tags.title": "{name}s utvalda hashtaggar",
   "account.follow": "Följ",
   "account.follow_back": "Följ tillbaka",
   "account.followers": "Följare",
@@ -65,6 +64,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} inlägg} other {{counter} inlägg}}",
   "account.unblock": "Avblockera @{name}",
   "account.unblock_domain": "Avblockera {domain}",
+  "account.unblock_domain_short": "Avblockera",
   "account.unblock_short": "Avblockera",
   "account.unendorse": "Visa inte på profil",
   "account.unfollow": "Sluta följ",
@@ -86,6 +86,13 @@
   "alert.unexpected.message": "Ett oväntat fel uppstod.",
   "alert.unexpected.title": "Hoppsan!",
   "alt_text_badge.title": "Alt-Text",
+  "alt_text_modal.add_alt_text": "Lägg till alt-text",
+  "alt_text_modal.add_text_from_image": "Lägg till text från bild",
+  "alt_text_modal.cancel": "Avbryt",
+  "alt_text_modal.change_thumbnail": "Ändra miniatyr",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "Beskriv detta för personer med hörselnedsättning…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "Beskriv detta för personer med synnedsättning…",
+  "alt_text_modal.done": "Klar",
   "announcement.announcement": "Meddelande",
   "annual_report.summary.archetype.booster": "Häftighetsjägaren",
   "annual_report.summary.archetype.lurker": "Smygaren",
@@ -211,6 +218,10 @@
   "confirmations.logout.confirm": "Logga ut",
   "confirmations.logout.message": "Är du säker på att du vill logga ut?",
   "confirmations.logout.title": "Logga ut?",
+  "confirmations.missing_alt_text.confirm": "Lägg till alt-text",
+  "confirmations.missing_alt_text.message": "Ditt inlägg innehåller media utan alt-text. Om du lägger till beskrivningar blir ditt innehåll tillgängligt för fler personer.",
+  "confirmations.missing_alt_text.secondary": "Posta ändå",
+  "confirmations.missing_alt_text.title": "Lägg till alt-text?",
   "confirmations.mute.confirm": "Tysta",
   "confirmations.redraft.confirm": "Radera & gör om",
   "confirmations.redraft.message": "Är du säker på att du vill radera detta inlägg och göra om det? Favoritmarkeringar, boostar och svar till det ursprungliga inlägget kommer förlora sitt sammanhang.",
@@ -220,7 +231,7 @@
   "confirmations.reply.title": "Skriva över inlägget?",
   "confirmations.unfollow.confirm": "Avfölj",
   "confirmations.unfollow.message": "Är du säker på att du vill avfölja {name}?",
-  "confirmations.unfollow.title": "Avfölj %s?",
+  "confirmations.unfollow.title": "Avfölj användare?",
   "content_warning.hide": "Dölj inlägg",
   "content_warning.show": "Visa ändå",
   "content_warning.show_more": "Visa mer",
@@ -407,6 +418,8 @@
   "ignore_notifications_modal.not_followers_title": "Vill du ignorera aviseringar från personer som inte följer dig?",
   "ignore_notifications_modal.not_following_title": "Vill du blockera aviseringar från personer som du inte följer dig?",
   "ignore_notifications_modal.private_mentions_title": "Vill du ignorera aviseringar från oombedda privata omnämnanden?",
+  "info_button.label": "Hjälp",
+  "info_button.what_is_alt_text": "<h1>Vad är alt-text?</h1> <p>alt-text ger bildbeskrivningar för personer med synnedsättning, anslutningar med låg bandbredd eller de som söker extra sammanhang.</p> <p>Du kan förbättra tillgängligheten och förståelsen för alla genom att skriva en tydlig, koncis och objektiv alt-text.</p> <ul> <li>Fånga viktiga element</li> <li>Sammanfatta text i bilder</li> <li>Använd vanlig meningsstruktur</li> <li>Undvik överflödig information</li> <li>Fokus på trender och viktiga resultat i komplexa bilder (som diagram eller kartor)</li> </ul>",
   "interaction_modal.action.favourite": "För att fortsätta, måste du favoritmarkera från ditt konto.",
   "interaction_modal.action.follow": "För att fortsätta, måste du följa från ditt konto.",
   "interaction_modal.action.reblog": "För att fortsätta, måste du boosta från ditt konto.",
@@ -684,7 +697,7 @@
   "poll_button.remove_poll": "Ta bort omröstning",
   "privacy.change": "Ändra inläggsintegritet",
   "privacy.direct.long": "Alla som nämns i inlägget",
-  "privacy.direct.short": "Särskilda personer",
+  "privacy.direct.short": "Privat omnämnande",
   "privacy.private.long": "Endast dina följare",
   "privacy.private.short": "Följare",
   "privacy.public.long": "Alla på och utanför Mastodon",
@@ -890,8 +903,12 @@
   "video.expand": "Expandera video",
   "video.fullscreen": "Helskärm",
   "video.hide": "Dölj video",
-  "video.mute": "Stäng av ljud",
+  "video.mute": "Tysta",
   "video.pause": "Pausa",
   "video.play": "Spela upp",
-  "video.unmute": "Spela upp ljud"
+  "video.skip_backward": "Hoppa bakåt",
+  "video.skip_forward": "Hoppa framåt",
+  "video.unmute": "Avtysta",
+  "video.volume_down": "Volym ned",
+  "video.volume_up": "Volym upp"
 }
diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json
index 1885c3a4ee..69c0029b7f 100644
--- a/app/javascript/mastodon/locales/ta.json
+++ b/app/javascript/mastodon/locales/ta.json
@@ -367,8 +367,6 @@
   "video.expand": "வீடியோவை விரிவாக்கு",
   "video.fullscreen": "முழுத்திரை",
   "video.hide": "வீடியோவை மறை",
-  "video.mute": "ஒலி முடக்கவும்",
   "video.pause": "இடைநிறுத்து",
-  "video.play": "விளையாடு",
-  "video.unmute": "ஒலி மெளனமாக இல்லை"
+  "video.play": "விளையாடு"
 }
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index e734d9b37c..959c380787 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -257,8 +257,6 @@
   "video.expand": "వీడియోను విస్తరించండి",
   "video.fullscreen": "పూర్తి స్క్రీన్",
   "video.hide": "వీడియోను దాచు",
-  "video.mute": "ధ్వనిని మ్యూట్ చేయి",
   "video.pause": "పాజ్ చేయి",
-  "video.play": "ప్లే చేయి",
-  "video.unmute": "ధ్వనిని అన్మ్యూట్ చేయి"
+  "video.play": "ప్లే చేయి"
 }
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 2959572ebc..429d0db428 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -29,7 +29,6 @@
   "account.endorse": "แสดงในโปรไฟล์",
   "account.featured_tags.last_status_at": "โพสต์ล่าสุดเมื่อ {date}",
   "account.featured_tags.last_status_never": "ไม่มีโพสต์",
-  "account.featured_tags.title": "แฮชแท็กที่น่าสนใจของ {name}",
   "account.follow": "ติดตาม",
   "account.follow_back": "ติดตามกลับ",
   "account.followers": "ผู้ติดตาม",
@@ -65,6 +64,7 @@
   "account.statuses_counter": "{count, plural, other {{counter} โพสต์}}",
   "account.unblock": "เลิกปิดกั้น @{name}",
   "account.unblock_domain": "เลิกปิดกั้นโดเมน {domain}",
+  "account.unblock_domain_short": "เลิกปิดกั้น",
   "account.unblock_short": "เลิกปิดกั้น",
   "account.unendorse": "ไม่แสดงในโปรไฟล์",
   "account.unfollow": "เลิกติดตาม",
@@ -86,6 +86,13 @@
   "alert.unexpected.message": "เกิดข้อผิดพลาดที่ไม่คาดคิด",
   "alert.unexpected.title": "อุปส์!",
   "alt_text_badge.title": "ข้อความแสดงแทน",
+  "alt_text_modal.add_alt_text": "เพิ่มข้อความแสดงแทน",
+  "alt_text_modal.add_text_from_image": "เพิ่มข้อความจากภาพ",
+  "alt_text_modal.cancel": "ยกเลิก",
+  "alt_text_modal.change_thumbnail": "เปลี่ยนภาพขนาดย่อ",
+  "alt_text_modal.describe_for_people_with_hearing_impairments": "อธิบายสิ่งนี้สำหรับผู้คนที่มีความบกพร่องทางการได้ยิน…",
+  "alt_text_modal.describe_for_people_with_visual_impairments": "อธิบายสิ่งนี้สำหรับผู้คนที่มีความบกพร่องทางการมองเห็น…",
+  "alt_text_modal.done": "เสร็จสิ้น",
   "announcement.announcement": "ประกาศ",
   "annual_report.summary.archetype.booster": "ผู้ล่าความเจ๋ง",
   "annual_report.summary.archetype.lurker": "ผู้ซุ่มอ่านข่าว",
@@ -208,6 +215,10 @@
   "confirmations.logout.confirm": "ออกจากระบบ",
   "confirmations.logout.message": "คุณแน่ใจหรือไม่ว่าต้องการออกจากระบบ?",
   "confirmations.logout.title": "ออกจากระบบ?",
+  "confirmations.missing_alt_text.confirm": "เพิ่มข้อความแสดงแทน",
+  "confirmations.missing_alt_text.message": "โพสต์ของคุณมีสื่อที่ไม่มีข้อความแสดงแทน การเพิ่มคำอธิบายช่วยทำให้เนื้อหาของคุณเข้าถึงผู้คนได้มากขึ้น",
+  "confirmations.missing_alt_text.secondary": "โพสต์ต่อไป",
+  "confirmations.missing_alt_text.title": "เพิ่มข้อความแสดงแทน?",
   "confirmations.mute.confirm": "ซ่อน",
   "confirmations.redraft.confirm": "ลบแล้วร่างใหม่",
   "confirmations.redraft.message": "คุณแน่ใจหรือไม่ว่าต้องการลบโพสต์นี้แล้วร่างโพสต์ใหม่? รายการโปรดและการดันจะสูญหาย และการตอบกลับโพสต์ดั้งเดิมจะไม่มีความเกี่ยวพัน",
@@ -400,6 +411,7 @@
   "ignore_notifications_modal.not_followers_title": "เพิกเฉยการแจ้งเตือนจากผู้คนที่ไม่ได้ติดตามคุณ?",
   "ignore_notifications_modal.not_following_title": "เพิกเฉยการแจ้งเตือนจากผู้คนที่คุณไม่ได้ติดตาม?",
   "ignore_notifications_modal.private_mentions_title": "เพิกเฉยการแจ้งเตือนจากการกล่าวถึงแบบส่วนตัวที่ไม่พึงประสงค์?",
+  "info_button.label": "ช่วยเหลือ",
   "interaction_modal.action.favourite": "เพื่อดำเนินการต่อ คุณจำเป็นต้องชื่นชอบจากบัญชีของคุณ",
   "interaction_modal.action.follow": "เพื่อดำเนินการต่อ คุณจำเป็นต้องติดตามจากบัญชีของคุณ",
   "interaction_modal.action.reblog": "เพื่อดำเนินการต่อ คุณจำเป็นต้องดันจากบัญชีของคุณ",
@@ -676,7 +688,7 @@
   "poll_button.remove_poll": "เอาการสำรวจความคิดเห็นออก",
   "privacy.change": "เปลี่ยนความเป็นส่วนตัวของโพสต์",
   "privacy.direct.long": "ทุกคนที่กล่าวถึงในโพสต์",
-  "privacy.direct.short": "ผู้คนที่เฉพาะเจาะจง",
+  "privacy.direct.short": "การกล่าวถึงแบบส่วนตัว",
   "privacy.private.long": "เฉพาะผู้ติดตามของคุณเท่านั้น",
   "privacy.private.short": "ผู้ติดตาม",
   "privacy.public.long": "ใครก็ตามที่อยู่ในและนอก Mastodon",
diff --git a/app/javascript/mastodon/locales/tok.json b/app/javascript/mastodon/locales/tok.json
index 7291588a38..08ce6fd324 100644
--- a/app/javascript/mastodon/locales/tok.json
+++ b/app/javascript/mastodon/locales/tok.json
@@ -29,7 +29,6 @@
   "account.endorse": "lipu jan la o suli e ni",
   "account.featured_tags.last_status_at": "sitelen pini pi jan ni li lon tenpo {date}",
   "account.featured_tags.last_status_never": "toki ala li lon",
-  "account.featured_tags.title": "{name} la kulupu ni pi toki suli li pona",
   "account.follow": "o kute",
   "account.follow_back": "jan ni li kute e sina. o kute",
   "account.followers": "jan kute",
@@ -65,7 +64,7 @@
   "account.statuses_counter": "{count, plural, other {toki {counter}}}",
   "account.unblock": "o weka ala e jan {name}",
   "account.unblock_domain": "o weka ala e ma {domain}",
-  "account.unblock_short": "o weka ala",
+  "account.unblock_short": "o pini weka",
   "account.unendorse": "lipu jan la o suli ala e ni",
   "account.unfollow": "o kute ala",
   "account.unmute": "o len ala e @{name}",
@@ -114,8 +113,8 @@
   "attachments_list.unprocessed": "(nasin open)",
   "audio.hide": "o len e kalama",
   "block_modal.remote_users_caveat": "mi pana e wile sina tawa ma {domain}. taso, o sona: ma li ken kepeken nasin len ante la pakala li ken lon. toki pi lukin ale la jan pi ma ala li ken lukin.",
-  "block_modal.show_less": "o lili e lukin",
-  "block_modal.show_more": "o suli e lukin",
+  "block_modal.show_less": "o pana e lili",
+  "block_modal.show_more": "o pana e mute",
   "block_modal.they_cant_mention": "ona li ken ala toki tawa sina li ken ala kute e sina.",
   "block_modal.they_cant_see_posts": "ona li ken ala lukin e toki sina. sina ken ala lukin e toki ona.",
   "block_modal.they_will_know": "ona li sona e ni: sina weka e lukin ona.",
@@ -215,6 +214,9 @@
   "confirmations.logout.confirm": "o weka",
   "confirmations.logout.message": "sina wile ala wile weka",
   "confirmations.logout.title": "o weka?",
+  "confirmations.missing_alt_text.confirm": "pana e toki pi sona lukin",
+  "confirmations.missing_alt_text.message": "toki ni la sitelen li lon. taso toki pi sona lukin li lon ala. toki pi sona lukin li pona tan ni: jan ale li ken sona e toki.",
+  "confirmations.missing_alt_text.title": "o pana e toki pi sona lukin",
   "confirmations.mute.confirm": "o len",
   "confirmations.redraft.confirm": "o weka o pali sin e toki",
   "confirmations.redraft.message": "pali sin e toki ni la sina wile ala wile weka e ona? sina ni la suli pi toki ni en wawa pi toki ni li weka. kin la toki lon toki ni li jo e mama ala.",
@@ -235,6 +237,7 @@
   "copy_icon_button.copied": "toki li awen lon ilo sina",
   "copypaste.copied": "sina jo e toki",
   "copypaste.copy_to_clipboard": "o awen lon ilo sina",
+  "directory.federated": "tan lipu ante sona",
   "directory.local": "tan {domain} taso",
   "directory.new_arrivals": "jan pi kama sin",
   "directory.recently_active": "jan lon tenpo poka",
@@ -243,6 +246,7 @@
   "dismissable_banner.community_timeline": "ni li toki pi tenpo poka tawa ale tan jan lon ma lawa pi nimi {domain}.",
   "dismissable_banner.dismiss": "o weka",
   "dismissable_banner.explore_links": "tenpo suno ni la jan pi kulupu ale li toki e ijo sin ni. ijo sin pi jan ante mute li sewi lon lipu ni.",
+  "dismissable_banner.explore_statuses": "jan mute li lukin e toki ni tan ma ilo weka. toki sin en toki pi wawa mute li lon sewi.",
   "domain_block_modal.block": "o weka e ma",
   "domain_block_modal.they_wont_know": "ona li sona ala e ni: sina weka e ona.",
   "domain_block_modal.title": "sina wile weka ala weka e ma?",
@@ -276,6 +280,7 @@
   "emoji_button.symbols": "sitelen",
   "emoji_button.travel": "ma en tawa",
   "empty_column.account_hides_collections": "jan ni li wile len e sona ni",
+  "empty_column.account_suspended": "lipu ni li weka",
   "empty_column.account_timeline": "toki ala li lon!",
   "empty_column.account_unavailable": "ken ala lukin e lipu jan",
   "empty_column.blocks": "jan ala li weka tawa sina.",
@@ -328,6 +333,7 @@
   "hashtag.counter_by_uses": "{count, plural, other {toki {counter}}}",
   "hashtag.follow": "o kute e kulupu lipu",
   "hashtag.unfollow": "o kute ala e kulupu lipu",
+  "home.column_settings.show_reblogs": "lukin e wawa",
   "home.pending_critical_update.link": "o lukin e ijo ilo sin",
   "info_button.label": "sona",
   "interaction_modal.go": "o tawa ma ni",
@@ -376,6 +382,7 @@
   "navigation_bar.about": "sona",
   "navigation_bar.blocks": "jan weka",
   "navigation_bar.compose": "o pali e toki sin",
+  "navigation_bar.domain_blocks": "kulupu pi ma weka",
   "navigation_bar.favourites": "ijo pona",
   "navigation_bar.filters": "nimi len",
   "navigation_bar.lists": "kulupu lipu",
@@ -472,6 +479,8 @@
   "status.pin": "o sewi lon lipu sina",
   "status.pinned": "toki sewi",
   "status.reblog": "o wawa",
+  "status.reblogged_by": "jan {name} li wawa",
+  "status.reblogs.empty": "jan ala li wawa e toki ni. jan li wawa la, nimi ona li sitelen lon ni.",
   "status.share": "o pana tawa ante",
   "status.show_less_all": "o lili e ale",
   "status.show_more_all": "o suli e ale",
@@ -498,7 +507,5 @@
   "video.exit_fullscreen": "o weka tan sitelen suli",
   "video.expand": "o suli e ni",
   "video.hide": "o len e sitelen",
-  "video.mute": "o kalama ala",
-  "video.pause": "o lape e ni",
-  "video.unmute": "o kalama"
+  "video.pause": "o lape e ni"
 }
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 589037af4d..1f703e0748 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Profili düzenle",
   "account.enable_notifications": "@{name} kişisinin gönderi bildirimlerini aç",
   "account.endorse": "Profilimde öne çıkar",
+  "account.featured": "Öne çıkan",
+  "account.featured.hashtags": "Etiketler",
+  "account.featured.posts": "Gönderiler",
   "account.featured_tags.last_status_at": "Son gönderinin tarihi {date}",
   "account.featured_tags.last_status_never": "Gönderi yok",
-  "account.featured_tags.title": "{name} kişisinin öne çıkan etiketleri",
   "account.follow": "Takip et",
   "account.follow_back": "Geri takip et",
   "account.followers": "Takipçi",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} gönderi} other {{counter} gönderi}}",
   "account.unblock": "@{name} adlı kişinin engelini kaldır",
   "account.unblock_domain": "{domain} alan adının engelini kaldır",
+  "account.unblock_domain_short": "Engeli kaldır",
   "account.unblock_short": "Engeli kaldır",
   "account.unendorse": "Profilimde öne çıkarma",
   "account.unfollow": "Takibi bırak",
@@ -148,7 +151,7 @@
   "column.bookmarks": "Yer İşaretleri",
   "column.community": "Yerel ağ akışı",
   "column.create_list": "Liste oluştur",
-  "column.direct": "Özel değinmeler",
+  "column.direct": "Özel mesajlar",
   "column.directory": "Profillere göz at",
   "column.domain_blocks": "Engellenen alan adları",
   "column.edit_list": "Listeyi düzenle",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "Arama sonuçları",
   "emoji_button.symbols": "Semboller",
   "emoji_button.travel": "Seyahat ve Yerler",
+  "empty_column.account_featured": "Bu liste boş",
   "empty_column.account_hides_collections": "Bu kullanıcı bu bilgiyi sağlamayı tercih etmemiştir",
   "empty_column.account_suspended": "Hesap askıya alındı",
   "empty_column.account_timeline": "Burada hiç gönderi yok!",
@@ -377,6 +381,8 @@
   "generic.saved": "Kaydet",
   "getting_started.heading": "Başlarken",
   "hashtag.admin_moderation": "#{name} için denetim arayüzünü açın",
+  "hashtag.browse": "#{hashtag} gönderilerine gözat",
+  "hashtag.browse_from_account": "@{name} kişisinin #{hashtag} gönderilerine gözat",
   "hashtag.column_header.tag_mode.all": "ve {additional}",
   "hashtag.column_header.tag_mode.any": "ya da {additional}",
   "hashtag.column_header.tag_mode.none": "{additional} olmadan",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} gönderi} other {{counter} gönderi}}",
   "hashtag.counter_by_uses_today": "bugün {count, plural, one {{counter} gönderi} other {{counter} gönderi}}",
   "hashtag.follow": "Etiketi takip et",
+  "hashtag.mute": "#{hashtag} gönderilerini sessize al",
   "hashtag.unfollow": "Etiketi takibi bırak",
   "hashtags.and_other": "…ve {count, plural, one {}other {# fazlası}}",
   "hints.profiles.followers_may_be_missing": "Bu profilin takipçileri eksik olabilir.",
@@ -444,7 +451,7 @@
   "keyboard_shortcuts.column": "Sütunlardan birindeki duruma odaklanmak için",
   "keyboard_shortcuts.compose": "Yazma alanına odaklanmak için",
   "keyboard_shortcuts.description": "Açıklama",
-  "keyboard_shortcuts.direct": "özel değinmeler sütununu açmak için",
+  "keyboard_shortcuts.direct": "özel mesajlar sütununu açmak için",
   "keyboard_shortcuts.down": "Listede aşağıya inmek için",
   "keyboard_shortcuts.enter": "Gönderiyi açınız",
   "keyboard_shortcuts.favourite": "Gönderiyi favorilerine ekle",
@@ -529,7 +536,7 @@
   "navigation_bar.bookmarks": "Yer İşaretleri",
   "navigation_bar.community_timeline": "Yerel ağ akışı",
   "navigation_bar.compose": "Yeni gönderi yaz",
-  "navigation_bar.direct": "Özel değinmeler",
+  "navigation_bar.direct": "Özel mesajlar",
   "navigation_bar.discover": "Keşfet",
   "navigation_bar.domain_blocks": "Engellenen alan adları",
   "navigation_bar.explore": "Keşfet",
@@ -662,10 +669,10 @@
   "notifications.policy.filter_not_following_hint": "Onları manuel olarak onaylayana kadar",
   "notifications.policy.filter_not_following_title": "Takip etmediğin kullanıcılar",
   "notifications.policy.filter_private_mentions_hint": "Kendi değinmenize yanıt veya takip ettiğiniz kullanıcıdan değilse filtrelenir",
-  "notifications.policy.filter_private_mentions_title": "İstenmeyen özel değinmeler",
+  "notifications.policy.filter_private_mentions_title": "İstenmeyen özel mesajlar",
   "notifications.policy.title": "Şundan bildirimleri yönet…",
   "notifications_permission_banner.enable": "Masaüstü bildirimlerini etkinleştir",
-  "notifications_permission_banner.how_to_control": "Mastodon açık olmadığında bildirim almak için masaüstü bildirimlerini etkinleştirin. Etkinleştirildikten sonra yukarıdaki {icon} düğmesini kullanarak hangi etkileşim türlerinin masaüstü bildirimleri oluşturduğunu tam olarak kontrol edebilirsiniz.",
+  "notifications_permission_banner.how_to_control": "Mastodon açık olmadığında bildirim almak için masaüstü bildirimlerini etkinleştirin. Etkinleştirildikten sonra, yukarıdaki{icon} düğmesi aracılığıyla hangi etkileşim türlerinin masaüstü bildirimi oluşturacağını tam olarak kontrol edebilirsiniz.",
   "notifications_permission_banner.title": "Hiçbir şeyi kaçırmayın",
   "onboarding.follows.back": "Geri",
   "onboarding.follows.done": "Tamamlandı",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "Anketi kaldır",
   "privacy.change": "Gönderi gizliliğini değiştir",
   "privacy.direct.long": "Gönderide değinilen herkes",
-  "privacy.direct.short": "Belirli kişiler",
+  "privacy.direct.short": "Özel bahsetme",
   "privacy.private.long": "Sadece takipçileriniz",
   "privacy.private.short": "Takipçiler",
   "privacy.public.long": "Mastodon'da olan olmayan herkes",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "{target} abone olduğu dilleri değiştir",
   "tabs_bar.home": "Anasayfa",
   "tabs_bar.notifications": "Bildirimler",
+  "terms_of_service.effective_as_of": "{date} itibariyle yürürlükte",
   "terms_of_service.title": "Hizmet Şartları",
+  "terms_of_service.upcoming_changes_on": "{date} tarihinde gerçekleşecek değişiklikler",
   "time_remaining.days": "{number, plural, one {# gün} other {# gün}} kaldı",
   "time_remaining.hours": "{number, plural, one {# saat} other {# saat}} kaldı",
   "time_remaining.minutes": "{number, plural, one {# dakika} other {# dakika}} kaldı",
@@ -903,8 +912,12 @@
   "video.expand": "Videoyu genişlet",
   "video.fullscreen": "Tam ekran",
   "video.hide": "Videoyu gizle",
-  "video.mute": "Sesi sustur",
+  "video.mute": "Sessiz",
   "video.pause": "Duraklat",
   "video.play": "Oynat",
-  "video.unmute": "Sesi aç"
+  "video.skip_backward": "Geriye atla",
+  "video.skip_forward": "İleriye atla",
+  "video.unmute": "Sesi aç",
+  "video.volume_down": "Sesi kıs",
+  "video.volume_up": "Sesi yükselt"
 }
diff --git a/app/javascript/mastodon/locales/tt.json b/app/javascript/mastodon/locales/tt.json
index c545711052..ee50180ced 100644
--- a/app/javascript/mastodon/locales/tt.json
+++ b/app/javascript/mastodon/locales/tt.json
@@ -19,6 +19,7 @@
   "account.block_short": "Блокла",
   "account.blocked": "Блокланган",
   "account.cancel_follow_request": "Киләсе сорау",
+  "account.copy": "Профиль сылтамасын күчереп ал",
   "account.disable_notifications": "@{name} язулары өчен белдерүләр сүндерү",
   "account.domain_blocked": "Домен блокланган",
   "account.edit_profile": "Профильне үзгәртү",
@@ -26,7 +27,6 @@
   "account.endorse": "Профильдә тәкъдим итү",
   "account.featured_tags.last_status_at": "Соңгы хәбәр {date}",
   "account.featured_tags.last_status_never": "Хәбәрләр юк",
-  "account.featured_tags.title": "{name} тәкъдим ителгән хэштеглар",
   "account.follow": "Язылу",
   "account.followers": "Язылучы",
   "account.followers.empty": "Әле беркем дә язылмаган.",
@@ -43,6 +43,8 @@
   "account.mention": "@{name} искәртү",
   "account.moved_to": "{name} аларның яңа счеты хәзер күрсәтте:",
   "account.mute": "@{name} кулланучыга әһәмият бирмәү",
+  "account.mute_notifications_short": "Искәртүләрне сүндер",
+  "account.mute_short": "Тавышсыз",
   "account.muted": "Әһәмият бирмәнгән",
   "account.open_original_page": "Чыганак битен ачу",
   "account.posts": "Язма",
@@ -58,6 +60,7 @@
   "account.unendorse": "Профильдә тәкъдим итмәү",
   "account.unfollow": "Язылуны туктату",
   "account.unmute": "Kабызыгыз @{name}",
+  "account.unmute_notifications_short": "Искәртүләрне кабыз",
   "account.unmute_short": "Kабызыгыз",
   "account_note.placeholder": "Click to add a note",
   "admin.dashboard.daily_retention": "Теркәлгәннән соң икенче көнне кулланучыларны тоту коэффициенты",
@@ -415,8 +418,6 @@
   "video.expand": "Видеоны җәю",
   "video.fullscreen": "Тулы экран",
   "video.hide": "Видеоны яшерү",
-  "video.mute": "Тавышны ябу",
   "video.pause": "Туктату",
-  "video.play": "Уйнату",
-  "video.unmute": "Тавышны ачу"
+  "video.play": "Уйнату"
 }
diff --git a/app/javascript/mastodon/locales/ug.json b/app/javascript/mastodon/locales/ug.json
index 378f688ba8..e550d7e678 100644
--- a/app/javascript/mastodon/locales/ug.json
+++ b/app/javascript/mastodon/locales/ug.json
@@ -1,21 +1,34 @@
 {
-  "about.blocks": "ئوتتۇراھال مۇلازىمېتىر",
-  "about.contact": "ئالاقىلاشقۇچى:",
-  "account.badges.bot": "Bot",
-  "account.cancel_follow_request": "Withdraw follow request",
-  "account.posts": "Toots",
-  "account.posts_with_replies": "Toots and replies",
+  "about.blocks": "باشقۇرۇلىدىغان مۇلازىمېتىر",
+  "about.contact": "ئالاقە:",
+  "about.disclaimer": "Mastodon ھەقسىز، ئوچۇق كودلۇق يۇمشاق دېتال تاۋار ماركىسى Mastodon gGmbH غا تەۋە.",
+  "about.domain_blocks.no_reason_available": "سەۋەبىنى ئىشلەتكىلى بولمايدۇ",
+  "account.badges.bot": "ماشىنا ئادەم",
+  "account.cancel_follow_request": "ئەگىشىش ئىلتىماسىدىن ۋاز كەچ",
+  "account.posts": "يازما",
+  "account.posts_with_replies": "يازما ۋە ئىنكاس",
+  "account.report": "@{name} نى پاش قىل",
   "account.requested": "Awaiting approval",
-  "account_note.placeholder": "Click to add a note",
-  "column.pins": "Pinned toot",
-  "community.column_settings.media_only": "Media only",
+  "account_note.placeholder": "چېكىلسە ئىزاھات قوشىدۇ",
+  "column.pins": "چوققىلانغان يازما",
+  "community.column_settings.media_only": "ۋاسىتەلا",
   "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
   "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
   "compose_form.placeholder": "What is on your mind?",
-  "compose_form.publish_form": "Publish",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
+  "compose_form.publish_form": "يېڭى يازما",
+  "compose_form.reply": "جاۋاب",
+  "compose_form.save_changes": "يېڭىلا",
+  "compose_form.spoiler.marked": "مەزمۇن ئاگاھلاندۇرۇشىنى چىقىرىۋەت",
   "compose_form.spoiler.unmarked": "Text is not hidden",
-  "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "compose_form.spoiler_placeholder": "مەزمۇن ئاگاھلاندۇرۇشى (تاللاشچان)",
+  "confirmation_modal.cancel": "ۋاز كەچ",
+  "confirmations.block.confirm": "توس",
+  "confirmations.delete.message": "بۇ يازمىنى راستىنلا ئۆچۈرەمسىز؟",
+  "confirmations.delete.title": "يازما ئۆچۈرەمدۇ؟",
+  "confirmations.delete_list.confirm": "ئۆچۈر",
+  "confirmations.delete_list.message": "بۇ تىزىمنى راستتىنلا مەڭگۈلۈك ئۆچۈرەمسىز؟",
+  "confirmations.delete_list.title": "تىزىمنى ئۆچۈرەمدۇ؟",
+  "confirmations.discard_edit_media.confirm": "تاشلىۋەت",
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "empty_column.account_timeline": "No toots here!",
   "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index ce44f902f9..6d37e0e1db 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Редагувати профіль",
   "account.enable_notifications": "Повідомляти мене про дописи @{name}",
   "account.endorse": "Рекомендувати у моєму профілі",
+  "account.featured": "Рекомендоване",
+  "account.featured.hashtags": "Хештеги",
+  "account.featured.posts": "Дописи",
   "account.featured_tags.last_status_at": "Останній допис {date}",
   "account.featured_tags.last_status_never": "Немає дописів",
-  "account.featured_tags.title": "{name} виділяє хештеґи",
   "account.follow": "Підписатися",
   "account.follow_back": "Стежити також",
   "account.followers": "Підписники",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}",
   "account.unblock": "Розблокувати @{name}",
   "account.unblock_domain": "Розблокувати {domain}",
+  "account.unblock_domain_short": "Розблокувати",
   "account.unblock_short": "Розблокувати",
   "account.unendorse": "Не публікувати у профілі",
   "account.unfollow": "Відписатися",
@@ -219,6 +222,7 @@
   "confirmations.logout.message": "Ви впевнені, що хочете вийти?",
   "confirmations.logout.title": "Вийти?",
   "confirmations.missing_alt_text.confirm": "Додати альтернативний текст",
+  "confirmations.missing_alt_text.message": "У вашому дописі є медіа без альтернативного тексту. Додавання опису допоможе зробити ваші матеріали доступними для більшої кількості людей.",
   "confirmations.missing_alt_text.secondary": "Все одно опублікувати",
   "confirmations.missing_alt_text.title": "Додати альтернативний текст?",
   "confirmations.mute.confirm": "Приховати",
@@ -251,7 +255,7 @@
   "dismissable_banner.dismiss": "Відхилити",
   "dismissable_banner.explore_links": "Ці новини сьогодні найбільше поширюють у fediverse. Свіжіші новини, опубліковані більшою кількістю різних людей, оцінюються вище.",
   "dismissable_banner.explore_statuses": "Ці дописи з усього fediverse сьогодні набирають популярності. Новіші дописи з більшою кількістю посилень і додавань у вибрані мають вищий рейтинг.",
-  "dismissable_banner.explore_tags": "Ці гештеґи сьогодні набувають популярності у fediverse. Гештеґи, якими користується більше людей, займають вищі позиції.",
+  "dismissable_banner.explore_tags": "Ці хештеги сьогодні набувають популярності у fediverse. Хештеги, якими користується більше людей, займають вищі позиції.",
   "dismissable_banner.public_timeline": "Це найновіші загальнодоступні дописи від людей у федіверсі, на яких підписані люди в {domain}.",
   "domain_block_modal.block": "Блокувати сервер",
   "domain_block_modal.block_account_instead": "Блокувати @{name} натомість",
@@ -292,6 +296,7 @@
   "emoji_button.search_results": "Результати пошуку",
   "emoji_button.symbols": "Символи",
   "emoji_button.travel": "Подорожі та місця",
+  "empty_column.account_featured": "Список порожній",
   "empty_column.account_hides_collections": "Цей користувач вирішив не робити цю інформацію доступною",
   "empty_column.account_suspended": "Обліковий запис заблоковано",
   "empty_column.account_timeline": "Тут немає дописів!",
@@ -389,6 +394,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}} сьогодні",
   "hashtag.follow": "Стежити за хештегом",
+  "hashtag.mute": "Ігнорувати #{hashtag}",
   "hashtag.unfollow": "Не стежити за хештегом",
   "hashtags.and_other": "…і {count, plural, other {ще #}}",
   "hints.profiles.followers_may_be_missing": "Підписники цього профілю можуть бути не показані.",
@@ -696,7 +702,7 @@
   "poll_button.remove_poll": "Видалити опитування",
   "privacy.change": "Змінити видимість допису",
   "privacy.direct.long": "Усі згадані в дописі",
-  "privacy.direct.short": "Певні люди",
+  "privacy.direct.short": "Особиста згадка",
   "privacy.private.long": "Лише ваші підписники",
   "privacy.private.short": "Підписники",
   "privacy.public.long": "Усі з Mastodon",
@@ -871,7 +877,9 @@
   "subscribed_languages.target": "Змінити підписані мови для {target}",
   "tabs_bar.home": "Головна",
   "tabs_bar.notifications": "Сповіщення",
+  "terms_of_service.effective_as_of": "Ефективний на {date}",
   "terms_of_service.title": "Умови використання",
+  "terms_of_service.upcoming_changes_on": "Майбутні зміни {date}",
   "time_remaining.days": "{number, plural, one {# день} few {# дні} other {# днів}}",
   "time_remaining.hours": "{number, plural, one {# година} few {# години} other {# годин}}",
   "time_remaining.minutes": "{number, plural, one {# хвилина} few {# хвилини} other {# хвилин}}",
@@ -902,8 +910,6 @@
   "video.expand": "Розгорнути відео",
   "video.fullscreen": "На весь екран",
   "video.hide": "Приховати відео",
-  "video.mute": "Вимкнути звук",
   "video.pause": "Призупинити",
-  "video.play": "Програвати",
-  "video.unmute": "Увімкнути звук"
+  "video.play": "Програвати"
 }
diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json
index cd50b512b4..e28ad93828 100644
--- a/app/javascript/mastodon/locales/ur.json
+++ b/app/javascript/mastodon/locales/ur.json
@@ -22,7 +22,6 @@
   "account.endorse": "مشکص پر نمایاں کریں",
   "account.featured_tags.last_status_at": "آخری پوسٹ {date} کو",
   "account.featured_tags.last_status_never": "کوئی مراسلہ نہیں",
-  "account.featured_tags.title": "{name} کے نمایاں ہیش ٹیگز",
   "account.follow": "پیروی کریں",
   "account.follow_back": "اکاؤنٹ کو فالو بیک ",
   "account.followers": "پیروکار",
diff --git a/app/javascript/mastodon/locales/uz.json b/app/javascript/mastodon/locales/uz.json
index e58ab35444..6dd350651d 100644
--- a/app/javascript/mastodon/locales/uz.json
+++ b/app/javascript/mastodon/locales/uz.json
@@ -25,7 +25,6 @@
   "account.endorse": "Profildagi xususiyat",
   "account.featured_tags.last_status_at": "Oxirgi post: {date}",
   "account.featured_tags.last_status_never": "Habarlar yo'q",
-  "account.featured_tags.title": "{name} ning taniqli hashtaglari",
   "account.follow": "Obuna bo‘lish",
   "account.followers": "Obunachilar",
   "account.followers.empty": "Bu foydalanuvchini hali hech kim kuzatmaydi.",
diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json
index 2fe4f70950..cabf3a5a82 100644
--- a/app/javascript/mastodon/locales/vi.json
+++ b/app/javascript/mastodon/locales/vi.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "Sửa hồ sơ",
   "account.enable_notifications": "Nhận thông báo khi @{name} đăng tút",
   "account.endorse": "Tôn vinh người này",
+  "account.featured": "Nổi bật",
+  "account.featured.hashtags": "Hashtag",
+  "account.featured.posts": "Tút",
   "account.featured_tags.last_status_at": "Tút gần nhất {date}",
   "account.featured_tags.last_status_never": "Chưa có tút",
-  "account.featured_tags.title": "Hashtag của {name}",
   "account.follow": "Theo dõi",
   "account.follow_back": "Theo dõi lại",
   "account.followers": "Người theo dõi",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, other {{counter} Tút}}",
   "account.unblock": "Bỏ chặn @{name}",
   "account.unblock_domain": "Bỏ ẩn {domain}",
+  "account.unblock_domain_short": "Bỏ chặn",
   "account.unblock_short": "Bỏ chặn",
   "account.unendorse": "Ngưng tôn vinh người này",
   "account.unfollow": "Bỏ theo dõi",
@@ -218,6 +221,10 @@
   "confirmations.logout.confirm": "Đăng xuất",
   "confirmations.logout.message": "Bạn có chắc muốn thoát?",
   "confirmations.logout.title": "Đăng xuất",
+  "confirmations.missing_alt_text.confirm": "Thêm văn bản thay thế",
+  "confirmations.missing_alt_text.message": "Tút của bạn chứa media không có văn bản thay thế. Thêm mô tả giúp nội dung của bạn dễ tiếp cận với nhiều người hơn.",
+  "confirmations.missing_alt_text.secondary": "Đăng luôn",
+  "confirmations.missing_alt_text.title": "Thêm văn bản thay thế?",
   "confirmations.mute.confirm": "Ẩn",
   "confirmations.redraft.confirm": "Xóa & viết lại",
   "confirmations.redraft.message": "Điều này sẽ khiến những lượt thích và đăng lại của tút bị mất, cũng như những trả lời sẽ không còn nội dung gốc.",
@@ -289,6 +296,7 @@
   "emoji_button.search_results": "Kết quả tìm kiếm",
   "emoji_button.symbols": "Biểu tượng",
   "emoji_button.travel": "Du lịch",
+  "empty_column.account_featured": "Danh sách trống",
   "empty_column.account_hides_collections": "Người này đã chọn ẩn thông tin",
   "empty_column.account_suspended": "Tài khoản vô hiệu hóa",
   "empty_column.account_timeline": "Chưa có tút nào!",
@@ -373,6 +381,8 @@
   "generic.saved": "Đã lưu",
   "getting_started.heading": "Quản lý",
   "hashtag.admin_moderation": "Mở giao diện quản trị #{name}",
+  "hashtag.browse": "Tìm tút #{hashtag}",
+  "hashtag.browse_from_account": "Tìm tút của @{name} có chứa #{hashtag}",
   "hashtag.column_header.tag_mode.all": "và {additional}",
   "hashtag.column_header.tag_mode.any": "hoặc {additional}",
   "hashtag.column_header.tag_mode.none": "mà không {additional}",
@@ -386,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, other {{counter} tút}}",
   "hashtag.counter_by_uses_today": "{count, plural, other {{counter} tút}} hôm nay",
   "hashtag.follow": "Theo dõi hashtag",
+  "hashtag.mute": "Ẩn #{hashtag}",
   "hashtag.unfollow": "Bỏ theo dõi hashtag",
   "hashtags.and_other": "…và {count, plural, other {# nữa}}",
   "hints.profiles.followers_may_be_missing": "Số người theo dõi có thể không đầy đủ.",
@@ -414,6 +425,8 @@
   "ignore_notifications_modal.not_followers_title": "Bỏ qua thông báo từ những người chưa theo dõi bạn?",
   "ignore_notifications_modal.not_following_title": "Bỏ qua thông báo từ những người bạn không theo dõi?",
   "ignore_notifications_modal.private_mentions_title": "Bỏ qua thông báo từ những lượt Nhắn Riêng không mong muốn?",
+  "info_button.label": "Trợ giúp",
+  "info_button.what_is_alt_text": "<h1>Văn bản thay thế là gì?</h1> <p>Văn bản thay thế giúp mô tả hình ảnh cho những người khiếm thị, kết nối mạng chậm hoặc những người muốn biết ngữ cảnh bổ sung.</p> <p>Bạn có thể cải thiện khả năng tiếp cận và giải thích kỹ hơn cho mọi người bằng cách viết văn bản thay thế rõ ràng, ngắn gọn và khách quan.</p> <ul> <li>Nắm bắt thành phần quan trọng</li> <li>Tóm tắt văn bản trong hình</li> <li>Dùng cấu trúc câu đơn</li> <li>Tránh giải thích rối rắmn</li> <li>Tập trung vào các xu hướng và phát hiện chính trong hình ảnh phức tạp (như biểu đồ hoặc bản đồ)</li> </ul>",
   "interaction_modal.action.favourite": "Để thích, bạn cần dùng tài khoản của bạn.",
   "interaction_modal.action.follow": "Để theo dõi, bạn cần dùng tài khoản của bạn.",
   "interaction_modal.action.reblog": "Để đăng lại, bạn cần dùng tài khoản của bạn.",
@@ -691,7 +704,7 @@
   "poll_button.remove_poll": "Xóa bình chọn",
   "privacy.change": "Chọn kiểu tút",
   "privacy.direct.long": "Những người được nhắc trong tút",
-  "privacy.direct.short": "Người cụ thể",
+  "privacy.direct.short": "Nhắn riêng",
   "privacy.private.long": "Chỉ người theo dõi",
   "privacy.private.short": "Người theo dõi",
   "privacy.public.long": "Bất cứ ai",
@@ -737,12 +750,12 @@
   "report.mute_explanation": "Bạn sẽ không còn thấy tút của người này. Họ vẫn có thể thấy tút của bạn hoặc theo dõi bạn. Họ không biết là bạn đã chặn họ.",
   "report.next": "Tiếp theo",
   "report.placeholder": "Thêm lưu ý",
-  "report.reasons.dislike": "Tôi không thích nó",
-  "report.reasons.dislike_description": "Đó không phải là thứ gì mà bạn muốn thấy",
+  "report.reasons.dislike": "Tôi không thích",
+  "report.reasons.dislike_description": "Đây không phải thứ mà bạn muốn thấy",
   "report.reasons.legal": "Vi phạm pháp luật",
   "report.reasons.legal_description": "Vi phạm pháp luật ở nơi đặt máy chủ hoặc nước bạn",
-  "report.reasons.other": "Một lý do khác",
-  "report.reasons.other_description": "Vấn đề không nằm trong những mục trên",
+  "report.reasons.other": "Lý do khác",
+  "report.reasons.other_description": "Vấn đề không thuộc những mục trên",
   "report.reasons.spam": "Đây là spam",
   "report.reasons.spam_description": "Liên kết độc hại, giả tương tác hoặc trả lời lặp đi lặp lại",
   "report.reasons.violation": "Vi phạm nội quy máy chủ",
@@ -866,7 +879,9 @@
   "subscribed_languages.target": "Đổi ngôn ngữ mong muốn cho {target}",
   "tabs_bar.home": "Trang chủ",
   "tabs_bar.notifications": "Thông báo",
+  "terms_of_service.effective_as_of": "Có hiệu lực vào {date}",
   "terms_of_service.title": "Điều khoản Dịch vụ",
+  "terms_of_service.upcoming_changes_on": "Sắp thay đổi vào {date}",
   "time_remaining.days": "{number, plural, other {# ngày}}",
   "time_remaining.hours": "{number, plural, other {# giờ}}",
   "time_remaining.minutes": "{number, plural, other {# phút}}",
@@ -900,5 +915,9 @@
   "video.mute": "Tắt tiếng",
   "video.pause": "Tạm dừng",
   "video.play": "Phát",
-  "video.unmute": "Mở tiếng"
+  "video.skip_backward": "Chuyển lùi lại",
+  "video.skip_forward": "Chuyển tới trước",
+  "video.unmute": "Bật tiếng",
+  "video.volume_down": "Giảm âm lượng",
+  "video.volume_up": "Tăng âm lượng"
 }
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index a096db73da..fc3f914e6a 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "修改个人资料",
   "account.enable_notifications": "当 @{name} 发布嘟文时通知我",
   "account.endorse": "在个人资料中推荐此用户",
+  "account.featured": "精选",
+  "account.featured.hashtags": "话题",
+  "account.featured.posts": "嘟文",
   "account.featured_tags.last_status_at": "上次发言于 {date}",
   "account.featured_tags.last_status_never": "暂无嘟文",
-  "account.featured_tags.title": "{name} 的精选标签",
   "account.follow": "关注",
   "account.follow_back": "回关",
   "account.followers": "关注者",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, other {{counter} 条嘟文}}",
   "account.unblock": "取消屏蔽 @{name}",
   "account.unblock_domain": "取消屏蔽 {domain} 域名",
+  "account.unblock_domain_short": "取消屏蔽",
   "account.unblock_short": "取消屏蔽",
   "account.unendorse": "不在个人资料中推荐此用户",
   "account.unfollow": "取消关注",
@@ -78,8 +81,8 @@
   "admin.dashboard.retention.cohort": "注册月份",
   "admin.dashboard.retention.cohort_size": "新用户",
   "admin.impact_report.instance_accounts": "将被删除的账号",
-  "admin.impact_report.instance_followers": "本实例用户即将丢失的关注者",
-  "admin.impact_report.instance_follows": "对方实例用户将会丢失的关注者",
+  "admin.impact_report.instance_followers": "本站用户即将丢失的关注者",
+  "admin.impact_report.instance_follows": "对方站点用户即将丢失的关注者",
   "admin.impact_report.title": "影响摘要",
   "alert.rate_limited.message": "请在 {retry_time, time, medium} 后重试。",
   "alert.rate_limited.title": "频率受限",
@@ -219,7 +222,7 @@
   "confirmations.logout.message": "确定要退出登录吗?",
   "confirmations.logout.title": "确定要退出登录?",
   "confirmations.missing_alt_text.confirm": "添加替代文本",
-  "confirmations.missing_alt_text.message": "您的帖子包含没有添加替代文本的媒体。添加描述有助于使更多人访问您的内容。",
+  "confirmations.missing_alt_text.message": "你的帖子包含没有替代文本的媒体。添加描述有助于使更多用户理解你的内容。",
   "confirmations.missing_alt_text.secondary": "就这样发布",
   "confirmations.missing_alt_text.title": "添加替代文本?",
   "confirmations.mute.confirm": "隐藏",
@@ -293,12 +296,13 @@
   "emoji_button.search_results": "搜索结果",
   "emoji_button.symbols": "符号",
   "emoji_button.travel": "旅行与地点",
+  "empty_column.account_featured": "这个列表为空",
   "empty_column.account_hides_collections": "该用户选择不公开此信息",
   "empty_column.account_suspended": "账号已被停用",
   "empty_column.account_timeline": "这里没有嘟文!",
   "empty_column.account_unavailable": "个人资料不可用",
   "empty_column.blocks": "你还未屏蔽任何用户。",
-  "empty_column.bookmarked_statuses": "你还没有收藏任何嘟文。收藏后嘟文就会显示在这里。",
+  "empty_column.bookmarked_statuses": "你还没有给任何嘟文添加书签。添加书签后的嘟文会显示在这里。",
   "empty_column.community": "本站时间线还没有内容,写点什么并公开发布,让它活跃起来吧!",
   "empty_column.direct": "你还未使用过私下提及。当你发出或者收到私下提及时,它将显示在此。",
   "empty_column.domain_blocks": "暂且没有被屏蔽的站点。",
@@ -390,6 +394,7 @@
   "hashtag.counter_by_uses": "{count, plural, other {{counter} 条嘟文}}",
   "hashtag.counter_by_uses_today": "今日 {count, plural, other {{counter} 条嘟文}}",
   "hashtag.follow": "关注话题",
+  "hashtag.mute": "停止提醒 #{hashtag}",
   "hashtag.unfollow": "取消关注话题",
   "hashtags.and_other": "… 和另外 {count, plural, other {# 个话题}}",
   "hints.profiles.followers_may_be_missing": "该账号的关注者列表可能没有完全显示。",
@@ -697,10 +702,10 @@
   "poll_button.remove_poll": "移除投票",
   "privacy.change": "设置嘟文的可见范围",
   "privacy.direct.long": "嘟文中提到的每个人",
-  "privacy.direct.short": "特定的人",
+  "privacy.direct.short": "私下提及",
   "privacy.private.long": "仅限你的关注者",
   "privacy.private.short": "关注者",
-  "privacy.public.long": "",
+  "privacy.public.long": "所有 Mastodon 内外的人",
   "privacy.public.short": "公开",
   "privacy.unlisted.additional": "此模式的行为与“公开”类似,只是嘟文不会出现在实时动态、话题、探索或 Mastodon 搜索页面中,即使您已全局开启了对应的发现设置。",
   "privacy.unlisted.long": "减少算法影响",
@@ -813,7 +818,7 @@
   "status.admin_domain": "打开 {domain} 的管理界面",
   "status.admin_status": "在管理界面查看此嘟文",
   "status.block": "屏蔽 @{name}",
-  "status.bookmark": "收藏",
+  "status.bookmark": "添加到书签",
   "status.cancel_reblog_private": "取消转嘟",
   "status.cannot_reblog": "不能转嘟这条嘟文",
   "status.continued_thread": "上接嘟文串",
@@ -849,7 +854,7 @@
   "status.reblogs": "{count, plural, other {次转嘟}}",
   "status.reblogs.empty": "没有人转嘟过此条嘟文。如果有人转嘟了,就会显示在这里。",
   "status.redraft": "删除并重新编辑",
-  "status.remove_bookmark": "取消收藏",
+  "status.remove_bookmark": "移除书签",
   "status.remove_favourite": "从喜欢列表中移除",
   "status.replied_in_thread": "回复嘟文串",
   "status.replied_to": "回复 {name}",
@@ -872,7 +877,9 @@
   "subscribed_languages.target": "更改 {target} 的订阅语言",
   "tabs_bar.home": "主页",
   "tabs_bar.notifications": "通知",
+  "terms_of_service.effective_as_of": "自 {date} 起生效",
   "terms_of_service.title": "服务条款",
+  "terms_of_service.upcoming_changes_on": "将于 {date} 进行变更",
   "time_remaining.days": "剩余 {number, plural, other {# 天}}",
   "time_remaining.hours": "剩余 {number, plural, other {# 小时}}",
   "time_remaining.minutes": "剩余 {number, plural, other {# 分钟}}",
@@ -903,8 +910,12 @@
   "video.expand": "展开视频",
   "video.fullscreen": "全屏",
   "video.hide": "隐藏视频",
-  "video.mute": "静音",
+  "video.mute": "停止提醒",
   "video.pause": "暂停",
   "video.play": "播放",
-  "video.unmute": "取消静音"
+  "video.skip_backward": "后退",
+  "video.skip_forward": "前进",
+  "video.unmute": "恢复提醒",
+  "video.volume_down": "音量减小",
+  "video.volume_up": "提高音量"
 }
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 264a75fefd..493d06b672 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -29,7 +29,6 @@
   "account.endorse": "在個人檔案中推薦對方",
   "account.featured_tags.last_status_at": "上次帖文於 {date}",
   "account.featured_tags.last_status_never": "暫無文章",
-  "account.featured_tags.title": "{name} 的精選標籤",
   "account.follow": "關注",
   "account.follow_back": "追蹤對方",
   "account.followers": "追蹤者",
@@ -84,6 +83,8 @@
   "alert.unexpected.message": "發生意外錯誤。",
   "alert.unexpected.title": "失敗!",
   "alt_text_badge.title": "替代文字",
+  "alt_text_modal.cancel": "取消",
+  "alt_text_modal.done": "完成",
   "announcement.announcement": "公告",
   "attachments_list.unprocessed": "(未處理)",
   "audio.hide": "隱藏音訊",
@@ -135,6 +136,7 @@
   "column_header.pin": "置頂",
   "column_header.show_settings": "顯示設定",
   "column_header.unpin": "取消置頂",
+  "column_search.cancel": "取消",
   "column_subheading.settings": "設定",
   "community.column_settings.local_only": "只顯示本站",
   "community.column_settings.media_only": "只顯示多媒體",
@@ -344,6 +346,7 @@
   "home.pending_critical_update.title": "有重要的安全更新!",
   "home.show_announcements": "顯示公告",
   "ignore_notifications_modal.ignore": "忽略推播通知",
+  "info_button.label": "幫助",
   "interaction_modal.on_another_server": "於不同伺服器",
   "interaction_modal.on_this_server": "於此伺服器",
   "interaction_modal.title.favourite": "把 {name} 的帖文加入最愛",
@@ -394,6 +397,7 @@
   "limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。",
   "link_preview.author": "由 {name} 提供",
   "lists.delete": "刪除列表",
+  "lists.done": "完成",
   "lists.edit": "編輯列表",
   "lists.replies_policy.followed": "任何已關注的用戶",
   "lists.replies_policy.list": "列表中的用戶",
@@ -461,6 +465,7 @@
   "notification.update": "{name} 編輯了帖文",
   "notification_requests.accept": "接受",
   "notification_requests.dismiss": "忽略",
+  "notification_requests.exit_selection": "完成",
   "notification_requests.notifications_from": "來自 {name} 的通知",
   "notification_requests.title": "已過濾之通知",
   "notifications.clear": "清空通知紀錄",
@@ -507,6 +512,7 @@
   "notifications_permission_banner.enable": "啟用桌面通知",
   "notifications_permission_banner.how_to_control": "只要啟用桌面通知,便可在 Mastodon 網站沒有打開時收到通知。在已經啟用桌面通知的時候,你可以透過上面的 {icon} 按鈕準確控制哪些類型的互動會產生桌面通知。",
   "notifications_permission_banner.title": "不放過任何事情",
+  "onboarding.follows.done": "完成",
   "onboarding.follows.empty": "很遺憾,現在無法顯示任何結果。你可以嘗試搜尋或瀏覽探索頁面來找使用者來追蹤,或者稍後再試。",
   "onboarding.profile.discoverable": "將個人檔案設為可被搜尋",
   "onboarding.profile.discoverable_hint": "當你在 Mastodon 上選擇可被搜尋時,你的帖文可能會出現在搜尋結果和熱門,你的個人檔案也可能被推薦給與你興趣相似的人。",
@@ -533,7 +539,6 @@
   "poll_button.remove_poll": "移除投票",
   "privacy.change": "調整私隱設定",
   "privacy.direct.long": "帖文提及的人",
-  "privacy.direct.short": "特定的人",
   "privacy.private.long": "只有你的追蹤者",
   "privacy.private.short": "追蹤者",
   "privacy.public.long": "Mastodon 內外的任何人",
@@ -717,8 +722,6 @@
   "video.expand": "展開影片",
   "video.fullscreen": "全螢幕",
   "video.hide": "隱藏影片",
-  "video.mute": "靜音",
   "video.pause": "暫停",
-  "video.play": "播放",
-  "video.unmute": "解除靜音"
+  "video.play": "播放"
 }
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index a91778a9fb..0d37e7f6c5 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -27,9 +27,11 @@
   "account.edit_profile": "編輯個人檔案",
   "account.enable_notifications": "當 @{name} 嘟文時通知我",
   "account.endorse": "於個人檔案推薦對方",
+  "account.featured": "精選內容",
+  "account.featured.hashtags": "主題標籤",
+  "account.featured.posts": "嘟文",
   "account.featured_tags.last_status_at": "上次嘟文於 {date}",
   "account.featured_tags.last_status_never": "沒有嘟文",
-  "account.featured_tags.title": "{name} 的推薦主題標籤",
   "account.follow": "跟隨",
   "account.follow_back": "跟隨回去",
   "account.followers": "跟隨者",
@@ -65,6 +67,7 @@
   "account.statuses_counter": "{count, plural, other {{count} 則嘟文}}",
   "account.unblock": "解除封鎖 @{name}",
   "account.unblock_domain": "解除封鎖網域 {domain}",
+  "account.unblock_domain_short": "解除封鎖",
   "account.unblock_short": "解除封鎖",
   "account.unendorse": "取消於個人檔案推薦對方",
   "account.unfollow": "取消跟隨",
@@ -293,6 +296,7 @@
   "emoji_button.search_results": "搜尋結果",
   "emoji_button.symbols": "符號",
   "emoji_button.travel": "旅遊與地點",
+  "empty_column.account_featured": "此列表為空",
   "empty_column.account_hides_collections": "這位使用者選擇不提供此資訊",
   "empty_column.account_suspended": "帳號已被停權",
   "empty_column.account_timeline": "這裡還沒有嘟文!",
@@ -377,6 +381,8 @@
   "generic.saved": "已儲存",
   "getting_started.heading": "開始使用",
   "hashtag.admin_moderation": "開啟 #{name} 的管理介面",
+  "hashtag.browse": "瀏覽於 #{hashtag} 之嘟文",
+  "hashtag.browse_from_account": "瀏覽來自 @{name} 於 #{hashtag} 之嘟文",
   "hashtag.column_header.tag_mode.all": "以及 {additional}",
   "hashtag.column_header.tag_mode.any": "或是 {additional}",
   "hashtag.column_header.tag_mode.none": "而無需 {additional}",
@@ -390,6 +396,7 @@
   "hashtag.counter_by_uses": "{count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
   "hashtag.counter_by_uses_today": "本日有 {count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
   "hashtag.follow": "跟隨主題標籤",
+  "hashtag.mute": "靜音 #{hashtag}",
   "hashtag.unfollow": "取消跟隨主題標籤",
   "hashtags.and_other": "…及其他 {count, plural, other {# 個}}",
   "hints.profiles.followers_may_be_missing": "此個人檔案之跟隨者或有缺失。",
@@ -697,7 +704,7 @@
   "poll_button.remove_poll": "移除投票",
   "privacy.change": "調整嘟文隱私狀態",
   "privacy.direct.long": "此嘟文提及之所有人",
-  "privacy.direct.short": "指定使用者",
+  "privacy.direct.short": "私訊",
   "privacy.private.long": "只有跟隨您之使用者能看到",
   "privacy.private.short": "跟隨者",
   "privacy.public.long": "所有人 (無論在 Mastodon 上與否)",
@@ -872,7 +879,9 @@
   "subscribed_languages.target": "變更 {target} 的訂閱語言",
   "tabs_bar.home": "首頁",
   "tabs_bar.notifications": "通知",
+  "terms_of_service.effective_as_of": "{date} 起生效",
   "terms_of_service.title": "服務條款",
+  "terms_of_service.upcoming_changes_on": "{date} 起即將發生之異動",
   "time_remaining.days": "剩餘 {number, plural, other {# 天}}",
   "time_remaining.hours": "剩餘{number, plural, other {# 小時}}",
   "time_remaining.minutes": "剩餘{number, plural, other {# 分鐘}}",
@@ -906,5 +915,9 @@
   "video.mute": "靜音",
   "video.pause": "暫停",
   "video.play": "播放",
-  "video.unmute": "解除靜音"
+  "video.skip_backward": "上一部",
+  "video.skip_forward": "下一部",
+  "video.unmute": "取消靜音",
+  "video.volume_down": "降低音量",
+  "video.volume_up": "提高音量"
 }
diff --git a/app/javascript/mastodon/models/alert.ts b/app/javascript/mastodon/models/alert.ts
new file mode 100644
index 0000000000..bc492eff3c
--- /dev/null
+++ b/app/javascript/mastodon/models/alert.ts
@@ -0,0 +1,14 @@
+import type { MessageDescriptor } from 'react-intl';
+
+export type TranslatableString = string | MessageDescriptor;
+
+export type TranslatableValues = Record<string, string | number | Date>;
+
+export interface Alert {
+  key: number;
+  title?: TranslatableString;
+  message: TranslatableString;
+  action?: TranslatableString;
+  values?: TranslatableValues;
+  onClick?: () => void;
+}
diff --git a/app/javascript/mastodon/models/dropdown_menu.ts b/app/javascript/mastodon/models/dropdown_menu.ts
new file mode 100644
index 0000000000..ceea9ad4dd
--- /dev/null
+++ b/app/javascript/mastodon/models/dropdown_menu.ts
@@ -0,0 +1,24 @@
+interface BaseMenuItem {
+  text: string;
+  dangerous?: boolean;
+}
+
+export interface ActionMenuItem extends BaseMenuItem {
+  action: () => void;
+}
+
+export interface LinkMenuItem extends BaseMenuItem {
+  to: string;
+}
+
+export interface ExternalLinkMenuItem extends BaseMenuItem {
+  href: string;
+  target?: string;
+  method?: 'post' | 'put' | 'delete';
+}
+
+export type MenuItem =
+  | ActionMenuItem
+  | LinkMenuItem
+  | ExternalLinkMenuItem
+  | null;
diff --git a/app/javascript/mastodon/models/poll.ts b/app/javascript/mastodon/models/poll.ts
index b4ba38a9c6..6f5655680d 100644
--- a/app/javascript/mastodon/models/poll.ts
+++ b/app/javascript/mastodon/models/poll.ts
@@ -1,6 +1,3 @@
-import type { RecordOf } from 'immutable';
-import { Record, List } from 'immutable';
-
 import escapeTextContentForBrowser from 'escape-html';
 
 import type { ApiPollJSON, ApiPollOptionJSON } from 'mastodon/api_types/polls';
@@ -9,19 +6,12 @@ import emojify from 'mastodon/features/emoji/emoji';
 import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
 import type { CustomEmoji, EmojiMap } from './custom_emoji';
 
-interface PollOptionTranslationShape {
+interface PollOptionTranslation {
   title: string;
   titleHtml: string;
 }
 
-export type PollOptionTranslation = RecordOf<PollOptionTranslationShape>;
-
-export const PollOptionTranslationFactory = Record<PollOptionTranslationShape>({
-  title: '',
-  titleHtml: '',
-});
-
-interface PollOptionShape extends Required<ApiPollOptionJSON> {
+export interface PollOption extends ApiPollOptionJSON {
   voted: boolean;
   titleHtml: string;
   translation: PollOptionTranslation | null;
@@ -31,45 +21,30 @@ export function createPollOptionTranslationFromServerJSON(
   translation: { title: string },
   emojiMap: EmojiMap,
 ) {
-  return PollOptionTranslationFactory({
+  return {
     ...translation,
     titleHtml: emojify(
       escapeTextContentForBrowser(translation.title),
       emojiMap,
     ),
-  });
+  } as PollOptionTranslation;
 }
 
-export type PollOption = RecordOf<PollOptionShape>;
-
-export const PollOptionFactory = Record<PollOptionShape>({
-  title: '',
-  votes_count: 0,
-  voted: false,
-  titleHtml: '',
-  translation: null,
-});
-
-interface PollShape
+export interface Poll
   extends Omit<ApiPollJSON, 'emojis' | 'options' | 'own_votes'> {
-  emojis: List<CustomEmoji>;
-  options: List<PollOption>;
-  own_votes?: List<number>;
+  emojis: CustomEmoji[];
+  options: PollOption[];
+  own_votes?: number[];
 }
-export type Poll = RecordOf<PollShape>;
 
-export const PollFactory = Record<PollShape>({
-  id: '',
-  expires_at: '',
+const pollDefaultValues = {
   expired: false,
   multiple: false,
   voters_count: 0,
   votes_count: 0,
   voted: false,
-  emojis: List<CustomEmoji>(),
-  options: List<PollOption>(),
-  own_votes: List(),
-});
+  own_votes: [],
+};
 
 export function createPollFromServerJSON(
   serverJSON: ApiPollJSON,
@@ -77,33 +52,31 @@ export function createPollFromServerJSON(
 ) {
   const emojiMap = makeEmojiMap(serverJSON.emojis);
 
-  return PollFactory({
+  return {
+    ...pollDefaultValues,
     ...serverJSON,
-    emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))),
-    own_votes: serverJSON.own_votes ? List(serverJSON.own_votes) : undefined,
-    options: List(
-      serverJSON.options.map((optionJSON, index) => {
-        const option = PollOptionFactory({
-          ...optionJSON,
-          voted: serverJSON.own_votes?.includes(index) || false,
-          titleHtml: emojify(
-            escapeTextContentForBrowser(optionJSON.title),
-            emojiMap,
-          ),
-        });
+    emojis: serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)),
+    options: serverJSON.options.map((optionJSON, index) => {
+      const option = {
+        ...optionJSON,
+        voted: serverJSON.own_votes?.includes(index) || false,
+        titleHtml: emojify(
+          escapeTextContentForBrowser(optionJSON.title),
+          emojiMap,
+        ),
+      } as PollOption;
 
-        const prevOption = previousPoll?.options.get(index);
-        if (prevOption?.translation && prevOption.title === option.title) {
-          const { translation } = prevOption;
+      const prevOption = previousPoll?.options[index];
+      if (prevOption?.translation && prevOption.title === option.title) {
+        const { translation } = prevOption;
 
-          option.set(
-            'translation',
-            createPollOptionTranslationFromServerJSON(translation, emojiMap),
-          );
-        }
+        option.translation = createPollOptionTranslationFromServerJSON(
+          translation,
+          emojiMap,
+        );
+      }
 
-        return option;
-      }),
-    ),
-  });
+      return option;
+    }),
+  } as Poll;
 }
diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js
deleted file mode 100644
index 1ca9b62a02..0000000000
--- a/app/javascript/mastodon/reducers/alerts.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { List as ImmutableList } from 'immutable';
-
-import {
-  ALERT_SHOW,
-  ALERT_DISMISS,
-  ALERT_CLEAR,
-} from '../actions/alerts';
-
-const initialState = ImmutableList([]);
-
-let id = 0;
-
-const addAlert = (state, alert) =>
-  state.push({
-    key: id++,
-    ...alert,
-  });
-
-export default function alerts(state = initialState, action) {
-  switch(action.type) {
-  case ALERT_SHOW:
-    return addAlert(state, action.alert);
-  case ALERT_DISMISS:
-    return state.filterNot(item => item.key === action.alert.key);
-  case ALERT_CLEAR:
-    return state.clear();
-  default:
-    return state;
-  }
-}
diff --git a/app/javascript/mastodon/reducers/alerts.ts b/app/javascript/mastodon/reducers/alerts.ts
new file mode 100644
index 0000000000..30108744ae
--- /dev/null
+++ b/app/javascript/mastodon/reducers/alerts.ts
@@ -0,0 +1,24 @@
+import { createReducer } from '@reduxjs/toolkit';
+
+import { showAlert, dismissAlert, clearAlerts } from 'mastodon/actions/alerts';
+import type { Alert } from 'mastodon/models/alert';
+
+const initialState: Alert[] = [];
+
+let id = 0;
+
+export const alertsReducer = createReducer(initialState, (builder) => {
+  builder
+    .addCase(showAlert, (state, { payload }) => {
+      state.push({
+        key: id++,
+        ...payload,
+      });
+    })
+    .addCase(dismissAlert, (state, { payload: { key } }) => {
+      return state.filter((item) => item.key !== key);
+    })
+    .addCase(clearAlerts, () => {
+      return [];
+    });
+});
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 1f0011448f..1fd660e48b 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -147,6 +147,7 @@ function clearAll(state) {
     map.set('sensitive', state.get('default_sensitive'));
     map.set('language', state.get('default_language'));
     map.update('media_attachments', list => list.clear());
+    map.set('progress', 0);
     map.set('poll', null);
     map.set('idempotencyKey', uuid());
     normalizePrivacy(map);
@@ -163,6 +164,7 @@ function appendMedia(state, media, file) {
     map.update('media_attachments', list => list.push(media.set('unattached', true)));
     map.set('is_uploading', false);
     map.set('is_processing', false);
+    map.set('progress', 0);
     map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
     map.set('idempotencyKey', uuid());
     map.update('pending_media_attachments', n => n - 1);
@@ -342,12 +344,26 @@ const expiresInFromExpiresAt = expires_at => {
 
 const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
   prefix = prefix.toLowerCase();
+
   if (suggestions.length < 4) {
     const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
-    return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
-  } else {
-    return suggestions;
+    suggestions = suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
   }
+
+  // Prefer capitalization from personal history, unless personal history is all lower-case
+  const fixSuggestionCapitalization = (suggestion) => {
+    if (suggestion.type !== 'hashtag')
+      return suggestion;
+
+    const tagFromHistory = tagHistory.find((tag) => tag.localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) === 0);
+
+    if (!tagFromHistory || tagFromHistory.toLowerCase() === tagFromHistory)
+      return suggestion;
+
+    return { ...suggestion, name: tagFromHistory };
+  };
+
+  return suggestions.map(fixSuggestionCapitalization);
 };
 
 const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
@@ -382,6 +398,8 @@ const updatePoll = (state, index, value, maxOptions) => state.updateIn(['poll',
   return tmp;
 });
 
+const calculateProgress = (loaded, total) => Math.min(Math.round((loaded / total) * 100), 100);
+
 /** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
 export const composeReducer = (state = initialState, action) => {
   if (changeUploadCompose.fulfilled.match(action)) {
@@ -506,15 +524,19 @@ export const composeReducer = (state = initialState, action) => {
   case COMPOSE_UPLOAD_SUCCESS:
     return appendMedia(state, fromJS(action.media), action.file);
   case COMPOSE_UPLOAD_FAIL:
-    return state.set('is_uploading', false).set('is_processing', false).update('pending_media_attachments', n => n - 1);
+    return state
+      .set('is_uploading', false)
+      .set('is_processing', false)
+      .set('progress', 0)
+      .update('pending_media_attachments', n => n - 1);
   case COMPOSE_UPLOAD_UNDO:
     return removeMedia(state, action.media_id);
   case COMPOSE_UPLOAD_PROGRESS:
-    return state.set('progress', Math.round((action.loaded / action.total) * 100));
+    return state.set('progress', calculateProgress(action.loaded, action.total));
   case THUMBNAIL_UPLOAD_REQUEST:
     return state.set('isUploadingThumbnail', true);
   case THUMBNAIL_UPLOAD_PROGRESS:
-    return state.set('thumbnailProgress', Math.round((action.loaded / action.total) * 100));
+    return state.set('thumbnailProgress', calculateProgress(action.loaded, action.total));
   case THUMBNAIL_UPLOAD_FAIL:
     return state.set('isUploadingThumbnail', false);
   case THUMBNAIL_UPLOAD_SUCCESS:
@@ -602,9 +624,9 @@ export const composeReducer = (state = initialState, action) => {
 
       if (action.status.get('poll')) {
         map.set('poll', ImmutableMap({
-          options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
-          multiple: action.status.getIn(['poll', 'multiple']),
-          expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
+          options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
+          multiple: action.status.get('poll').multiple,
+          expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
         }));
       }
     });
@@ -638,9 +660,9 @@ export const composeReducer = (state = initialState, action) => {
 
       if (action.status.get('poll')) {
         map.set('poll', ImmutableMap({
-          options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
-          multiple: action.status.getIn(['poll', 'multiple']),
-          expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
+          options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
+          multiple: action.status.get('poll').multiple,
+          expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
         }));
       }
     });
diff --git a/app/javascript/mastodon/reducers/domain_lists.js b/app/javascript/mastodon/reducers/domain_lists.js
deleted file mode 100644
index 5f63c77f5d..0000000000
--- a/app/javascript/mastodon/reducers/domain_lists.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
-
-import {
-  DOMAIN_BLOCKS_FETCH_SUCCESS,
-  DOMAIN_BLOCKS_EXPAND_SUCCESS,
-  unblockDomainSuccess
-} from '../actions/domain_blocks';
-
-const initialState = ImmutableMap({
-  blocks: ImmutableMap({
-    items: ImmutableOrderedSet(),
-  }),
-});
-
-export default function domainLists(state = initialState, action) {
-  switch(action.type) {
-  case DOMAIN_BLOCKS_FETCH_SUCCESS:
-    return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
-  case DOMAIN_BLOCKS_EXPAND_SUCCESS:
-    return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next);
-  case unblockDomainSuccess.type:
-    return state.updateIn(['blocks', 'items'], set => set.delete(action.payload.domain));
-  default:
-    return state;
-  }
-}
diff --git a/app/javascript/mastodon/reducers/dropdown_menu.ts b/app/javascript/mastodon/reducers/dropdown_menu.ts
index 59e19bb16d..0e46f0ee80 100644
--- a/app/javascript/mastodon/reducers/dropdown_menu.ts
+++ b/app/javascript/mastodon/reducers/dropdown_menu.ts
@@ -3,15 +3,15 @@ import { createReducer } from '@reduxjs/toolkit';
 import { closeDropdownMenu, openDropdownMenu } from '../actions/dropdown_menu';
 
 interface DropdownMenuState {
-  openId: string | null;
+  openId: number | null;
   keyboard: boolean;
-  scrollKey: string | null;
+  scrollKey: string | undefined;
 }
 
 const initialState: DropdownMenuState = {
   openId: null,
   keyboard: false,
-  scrollKey: null,
+  scrollKey: undefined,
 };
 
 export const dropdownMenuReducer = createReducer(initialState, (builder) => {
@@ -27,7 +27,7 @@ export const dropdownMenuReducer = createReducer(initialState, (builder) => {
     .addCase(closeDropdownMenu, (state, { payload: { id } }) => {
       if (state.openId === id) {
         state.openId = null;
-        state.scrollKey = null;
+        state.scrollKey = undefined;
       }
     });
 });
diff --git a/app/javascript/mastodon/reducers/followed_tags.js b/app/javascript/mastodon/reducers/followed_tags.js
deleted file mode 100644
index afea8e3b35..0000000000
--- a/app/javascript/mastodon/reducers/followed_tags.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
-
-import {
-  FOLLOWED_HASHTAGS_FETCH_REQUEST,
-  FOLLOWED_HASHTAGS_FETCH_SUCCESS,
-  FOLLOWED_HASHTAGS_FETCH_FAIL,
-  FOLLOWED_HASHTAGS_EXPAND_REQUEST,
-  FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
-  FOLLOWED_HASHTAGS_EXPAND_FAIL,
-} from 'mastodon/actions/tags';
-
-const initialState = ImmutableMap({
-  items: ImmutableList(),
-  isLoading: false,
-  next: null,
-});
-
-export default function followed_tags(state = initialState, action) {
-  switch(action.type) {
-  case FOLLOWED_HASHTAGS_FETCH_REQUEST:
-    return state.set('isLoading', true);
-  case FOLLOWED_HASHTAGS_FETCH_SUCCESS:
-    return state.withMutations(map => {
-      map.set('items', fromJS(action.followed_tags));
-      map.set('isLoading', false);
-      map.set('next', action.next);
-    });
-  case FOLLOWED_HASHTAGS_FETCH_FAIL:
-    return state.set('isLoading', false);
-  case FOLLOWED_HASHTAGS_EXPAND_REQUEST:
-    return state.set('isLoading', true);
-  case FOLLOWED_HASHTAGS_EXPAND_SUCCESS:
-    return state.withMutations(map => {
-      map.update('items', set => set.concat(fromJS(action.followed_tags)));
-      map.set('isLoading', false);
-      map.set('next', action.next);
-    });
-  case FOLLOWED_HASHTAGS_EXPAND_FAIL:
-    return state.set('isLoading', false);
-  default:
-    return state;
-  }
-}
diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts
index fd83682e12..617d7a08d9 100644
--- a/app/javascript/mastodon/reducers/index.ts
+++ b/app/javascript/mastodon/reducers/index.ts
@@ -5,7 +5,7 @@ import { combineReducers } from 'redux-immutable';
 
 import { accountsReducer } from './accounts';
 import accounts_map from './accounts_map';
-import alerts from './alerts';
+import { alertsReducer } from './alerts';
 import announcements from './announcements';
 import { antennasReducer } from './antennas';
 import { bookmarkCategoriesReducer } from './bookmark_categories';
@@ -14,10 +14,8 @@ import { composeReducer } from './compose';
 import contexts from './contexts';
 import conversations from './conversations';
 import custom_emojis from './custom_emojis';
-import domain_lists from './domain_lists';
 import { dropdownMenuReducer } from './dropdown_menu';
 import filters from './filters';
-import followed_tags from './followed_tags';
 import height_cache from './height_cache';
 import history from './history';
 import { listsReducer } from './lists';
@@ -49,11 +47,10 @@ const reducers = {
   dropdownMenu: dropdownMenuReducer,
   timelines,
   meta,
-  alerts,
+  alerts: alertsReducer,
   loadingBar: loadingBarReducer,
   modal: modalReducer,
   user_lists,
-  domain_lists,
   status_lists,
   accounts: accountsReducer,
   accounts_map,
@@ -82,7 +79,6 @@ const reducers = {
   markers: markersReducer,
   picture_in_picture: pictureInPictureReducer,
   history,
-  followed_tags,
   reaction_deck,
   notificationPolicy: notificationPolicyReducer,
   notificationRequests: notificationRequestsReducer,
diff --git a/app/javascript/mastodon/reducers/polls.ts b/app/javascript/mastodon/reducers/polls.ts
index 9b9a5d2ff8..aadf6741c1 100644
--- a/app/javascript/mastodon/reducers/polls.ts
+++ b/app/javascript/mastodon/reducers/polls.ts
@@ -1,5 +1,4 @@
 import type { Reducer } from '@reduxjs/toolkit';
-import { Map as ImmutableMap } from 'immutable';
 
 import { importPolls } from 'mastodon/actions/importer/polls';
 import { makeEmojiMap } from 'mastodon/models/custom_emoji';
@@ -11,57 +10,48 @@ import {
   STATUS_TRANSLATE_UNDO,
 } from '../actions/statuses';
 
-const initialState = ImmutableMap<string, Poll>();
+const initialState: Record<string, Poll> = {};
 type PollsState = typeof initialState;
 
-const statusTranslateSuccess = (
-  state: PollsState,
-  pollTranslation: Poll | undefined,
-) => {
-  if (!pollTranslation) return state;
+const statusTranslateSuccess = (state: PollsState, pollTranslation?: Poll) => {
+  if (!pollTranslation) return;
 
-  return state.withMutations((map) => {
-    const poll = state.get(pollTranslation.id);
+  const poll = state[pollTranslation.id];
 
-    if (!poll) return;
+  if (!poll) return;
 
-    const emojiMap = makeEmojiMap(poll.emojis);
+  const emojiMap = makeEmojiMap(poll.emojis);
 
-    pollTranslation.options.forEach((item, index) => {
-      map.setIn(
-        [pollTranslation.id, 'options', index, 'translation'],
-        createPollOptionTranslationFromServerJSON(item, emojiMap),
-      );
-    });
+  pollTranslation.options.forEach((item, index) => {
+    const option = poll.options[index];
+    if (!option) return;
+
+    option.translation = createPollOptionTranslationFromServerJSON(
+      item,
+      emojiMap,
+    );
   });
 };
 
 const statusTranslateUndo = (state: PollsState, id: string) => {
-  return state.withMutations((map) => {
-    const options = map.get(id)?.options;
-
-    if (options) {
-      options.forEach((item, index) =>
-        map.deleteIn([id, 'options', index, 'translation']),
-      );
-    }
+  state[id]?.options.forEach((option) => {
+    option.translation = null;
   });
 };
 
 export const pollsReducer: Reducer<PollsState> = (
-  state = initialState,
+  draft = initialState,
   action,
 ) => {
   if (importPolls.match(action)) {
-    return state.withMutations((polls) => {
-      action.payload.polls.forEach((poll) => polls.set(poll.id, poll));
+    action.payload.polls.forEach((poll) => {
+      draft[poll.id] = poll;
     });
   } else if (action.type === STATUS_TRANSLATE_SUCCESS)
-    return statusTranslateSuccess(
-      state,
-      (action.translation as { poll?: Poll }).poll,
-    );
-  else if (action.type === STATUS_TRANSLATE_UNDO)
-    return statusTranslateUndo(state, action.pollId as string);
-  else return state;
+    statusTranslateSuccess(draft, (action.translation as { poll?: Poll }).poll);
+  else if (action.type === STATUS_TRANSLATE_UNDO) {
+    statusTranslateUndo(draft, action.pollId as string);
+  }
+
+  return draft;
 };
diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js
index 2e60d831b1..cc660af5c3 100644
--- a/app/javascript/mastodon/reducers/status_lists.js
+++ b/app/javascript/mastodon/reducers/status_lists.js
@@ -184,6 +184,7 @@ const removeOneFromAllBookmarkCategoriesById = (state, statusId) => {
   return s;
 };
 
+/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
 export default function statusLists(state = initialState, action) {
   switch(action.type) {
   case FAVOURITED_STATUSES_FETCH_REQUEST:
diff --git a/app/javascript/mastodon/selectors/accounts.ts b/app/javascript/mastodon/selectors/accounts.ts
index cee3a87bca..a33daee867 100644
--- a/app/javascript/mastodon/selectors/accounts.ts
+++ b/app/javascript/mastodon/selectors/accounts.ts
@@ -1,6 +1,7 @@
 import { createSelector } from '@reduxjs/toolkit';
 import { Record as ImmutableRecord } from 'immutable';
 
+import { me } from 'mastodon/initial_state';
 import { accountDefaultValues } from 'mastodon/models/account';
 import type { Account, AccountShape } from 'mastodon/models/account';
 import type { Relationship } from 'mastodon/models/relationship';
@@ -45,3 +46,16 @@ export function makeGetAccount() {
     },
   );
 }
+
+export const getAccountHidden = createSelector(
+  [
+    (state: RootState, id: string) => state.accounts.get(id)?.hidden,
+    (state: RootState, id: string) =>
+      state.relationships.get(id)?.following ||
+      state.relationships.get(id)?.requested,
+    (state: RootState, id: string) => id === me,
+  ],
+  (hidden, followingOrRequested, isSelf) => {
+    return hidden && !(isSelf || followingOrRequested);
+  },
+);
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 85773152f2..a03ee70e3f 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -6,6 +6,7 @@ import { me, isHideItem } from '../initial_state';
 import { getFilters } from './filters';
 
 export { makeGetAccount } from "./accounts";
+export { getStatusList, getSubStatusList } from "./statuses";
 
 export const makeGetStatus = () => {
   return createSelector(
@@ -17,9 +18,10 @@ export const makeGetStatus = () => {
       (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
       (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
       getFilters,
+      (_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType),
     ],
 
-    (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters) => {
+    (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters, warnInsteadOfHide) => {
       if (!statusBase || statusBase.get('isLoading')) {
         return null;
       }
@@ -36,6 +38,7 @@ export const makeGetStatus = () => {
       }
 
       let filtered = false;
+      let mediaFiltered = false;
       if ((accountReblog || accountBase).get('id') !== me && filters) {
         let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
         const quoteFilterResults = statusQuote?.get('filtered');
@@ -46,10 +49,16 @@ export const makeGetStatus = () => {
           }
         }
 
-        if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
+        if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
           return null;
         }
-        filterResults = filterResults.filter(result => filters.has(result.get('filter')));
+
+        let mediaFilters = filterResults.filter(result => filters.getIn([result.get('filter'), 'filter_action']) === 'blur');
+        if (!mediaFilters.isEmpty()) {
+          mediaFiltered = mediaFilters.map(result => filters.getIn([result.get('filter'), 'title']));
+        }
+
+        filterResults = filterResults.filter(result => filters.has(result.get('filter')) && filters.getIn([result.get('filter'), 'filter_action']) !== 'blur');
         if (!filterResults.isEmpty()) {
           filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
         }
@@ -60,6 +69,7 @@ export const makeGetStatus = () => {
         map.set('quote', statusQuote);
         map.set('account', accountBase);
         map.set('matched_filters', filtered);
+        map.set('matched_media_filters', mediaFiltered);
       });
     },
   );
@@ -75,28 +85,6 @@ export const makeGetPictureInPicture = () => {
   }));
 };
 
-const ALERT_DEFAULTS = {
-  dismissAfter: 5000,
-  style: false,
-};
-
-const formatIfNeeded = (intl, message, values) => {
-  if (typeof message === 'object') {
-    return intl.formatMessage(message, values);
-  }
-
-  return message;
-};
-
-export const getAlerts = createSelector([state => state.get('alerts'), (_, { intl }) => intl], (alerts, intl) =>
-  alerts.map(item => ({
-    ...ALERT_DEFAULTS,
-    ...item,
-    action: formatIfNeeded(intl, item.action, item.values),
-    title: formatIfNeeded(intl, item.title, item.values),
-    message: formatIfNeeded(intl, item.message, item.values),
-  })).toArray());
-
 export const makeGetNotification = () => createSelector([
   (_, base)             => base,
   (state, _, accountId) => state.getIn(['accounts', accountId]),
@@ -106,38 +94,3 @@ export const makeGetReport = () => createSelector([
   (_, base) => base,
   (state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]),
 ], (base, targetAccount) => base.set('target_account', targetAccount));
-
-export const getAccountGallery = createSelector([
-  (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
-  state       => state.get('statuses'),
-  (state, id) => state.getIn(['accounts', id]),
-], (statusIds, statuses, account) => {
-  let medias = ImmutableList();
-
-  statusIds.forEach(statusId => {
-    const status = statuses.get(statusId).set('account', account);
-    medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status)));
-  });
-
-  return medias;
-});
-
-export const getAccountHidden = createSelector([
-  (state, id) => state.getIn(['accounts', id, 'hidden']),
-  (state, id) => state.getIn(['relationships', id, 'following']) || state.getIn(['relationships', id, 'requested']),
-  (state, id) => id === me,
-], (hidden, followingOrRequested, isSelf) => {
-  return hidden && !(isSelf || followingOrRequested);
-});
-
-export const getStatusList = createSelector([
-  (state, type) => state.getIn(['status_lists', type, 'items']),
-], (items) => items.toList());
-
-export const getBookmarkCategoryStatusList = createSelector([
-  (state, bookmarkCategoryId) => state.getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'items']),
-], (items) => items ? items.toList() : ImmutableList());
-
-export const getCircleStatusList = createSelector([
-  (state, circleId) => state.getIn(['status_lists', 'circle_statuses', circleId, `items`]),
-], (items) => items ? items.toList() : ImmutableList());
diff --git a/app/javascript/mastodon/selectors/statuses.ts b/app/javascript/mastodon/selectors/statuses.ts
new file mode 100644
index 0000000000..d07cc93aec
--- /dev/null
+++ b/app/javascript/mastodon/selectors/statuses.ts
@@ -0,0 +1,33 @@
+import { createSelector } from '@reduxjs/toolkit';
+import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import { List as ImmutableList } from 'immutable';
+
+import type { RootState } from 'mastodon/store';
+
+export const getStatusList = createSelector(
+  [
+    (
+      state: RootState,
+      type:
+        | 'favourites'
+        | 'bookmarks'
+        | 'pins'
+        | 'trending'
+        | 'emoji_reactions',
+    ) =>
+      state.status_lists.getIn([type, 'items']) as ImmutableOrderedSet<string>,
+  ],
+  (items) => items.toList(),
+);
+
+export const getSubStatusList = createSelector(
+  [
+    (state: RootState, type: 'bookmark_category' | 'circle', id: string) =>
+      state.status_lists.getIn([
+        `${type}_statuses`,
+        id,
+        'items',
+      ]) as ImmutableOrderedSet<string> | null,
+  ],
+  (items) => (items ? items.toList() : ImmutableList()),
+);
diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js
index 3e39c9a4ed..f3d61e0195 100644
--- a/app/javascript/mastodon/service_worker/web_push_locales.js
+++ b/app/javascript/mastodon/service_worker/web_push_locales.js
@@ -1,6 +1,3 @@
-/* eslint-disable import/no-commonjs --
-   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */
-
 /* @preval */
 
 const fs   = require('fs');
diff --git a/app/javascript/mastodon/store/middlewares/errors.ts b/app/javascript/mastodon/store/middlewares/errors.ts
index 3ad3844d5b..b9efe9f2b4 100644
--- a/app/javascript/mastodon/store/middlewares/errors.ts
+++ b/app/javascript/mastodon/store/middlewares/errors.ts
@@ -12,19 +12,21 @@ import type { AsyncThunkRejectValue } from '../typed_functions';
 const defaultFailSuffix = 'FAIL';
 const isFailedAction = new RegExp(`${defaultFailSuffix}$`, 'g');
 
-interface ActionWithMaybeAlertParams extends Action, AsyncThunkRejectValue {}
-
 interface RejectedAction extends Action {
   payload: AsyncThunkRejectValue;
 }
 
+interface ActionWithMaybeAlertParams extends Action, AsyncThunkRejectValue {
+  payload?: AsyncThunkRejectValue;
+}
+
 function isRejectedActionWithPayload(
   action: unknown,
 ): action is RejectedAction {
   return isAsyncThunkAction(action) && isRejectedWithValue(action);
 }
 
-function isActionWithmaybeAlertParams(
+function isActionWithMaybeAlertParams(
   action: unknown,
 ): action is ActionWithMaybeAlertParams {
   return isAction(action);
@@ -40,11 +42,12 @@ export const errorsMiddleware: Middleware<{}, RootState> =
         showAlertForError(action.payload.error, action.payload.skipNotFound),
       );
     } else if (
-      isActionWithmaybeAlertParams(action) &&
-      !action.skipAlert &&
+      isActionWithMaybeAlertParams(action) &&
+      !(action.payload?.skipAlert || action.skipAlert) &&
       action.type.match(isFailedAction)
     ) {
-      dispatch(showAlertForError(action.error, action.skipNotFound));
+      const { error, skipNotFound } = action.payload ?? action;
+      dispatch(showAlertForError(error, skipNotFound));
     }
 
     return next(action);
diff --git a/app/javascript/mastodon/utils/filters.ts b/app/javascript/mastodon/utils/filters.ts
index 5d334fe509..479e1f44ab 100644
--- a/app/javascript/mastodon/utils/filters.ts
+++ b/app/javascript/mastodon/utils/filters.ts
@@ -7,6 +7,11 @@ export const toServerSideType = (columnType: string) => {
     case 'account':
     case 'explore':
       return columnType;
+    case 'detailed':
+      return 'thread';
+    case 'bookmarks':
+    case 'favourites':
+      return 'home';
     default:
       if (columnType.includes('list:') || columnType.includes('antenna:')) {
         return 'home';
diff --git a/app/javascript/material-icons/400-24px/extension-fill.svg b/app/javascript/material-icons/400-24px/extension-fill.svg
new file mode 100644
index 0000000000..f6e7de8cce
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/extension-fill.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M352-120H200q-33 0-56.5-23.5T120-200v-152q48 0 84-30.5t36-77.5q0-47-36-77.5T120-568v-152q0-33 23.5-56.5T200-800h160q0-42 29-71t71-29q42 0 71 29t29 71h160q33 0 56.5 23.5T800-720v160q42 0 71 29t29 71q0 42-29 71t-71 29v160q0 33-23.5 56.5T720-120H568q0-50-31.5-85T460-240q-45 0-76.5 35T352-120Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/extension.svg b/app/javascript/material-icons/400-24px/extension.svg
new file mode 100644
index 0000000000..16909a6307
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/extension.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M352-120H200q-33 0-56.5-23.5T120-200v-152q48 0 84-30.5t36-77.5q0-47-36-77.5T120-568v-152q0-33 23.5-56.5T200-800h160q0-42 29-71t71-29q42 0 71 29t29 71h160q33 0 56.5 23.5T800-720v160q42 0 71 29t29 71q0 42-29 71t-71 29v160q0 33-23.5 56.5T720-120H568q0-50-31.5-85T460-240q-45 0-76.5 35T352-120Zm-152-80h85q24-66 77-93t98-27q45 0 98 27t77 93h85v-240h80q8 0 14-6t6-14q0-8-6-14t-14-6h-80v-240H480v-80q0-8-6-14t-14-6q-8 0-14 6t-6 14v80H200v88q54 20 87 67t33 105q0 57-33 104t-87 68v88Zm260-260Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/forward_5-fill.svg b/app/javascript/material-icons/400-24px/forward_5-fill.svg
new file mode 100644
index 0000000000..bc0119a640
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/forward_5-fill.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-440q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-800h6l-62-62 56-58 160 160-160 160-56-58 62-62h-6q-117 0-198.5 81.5T200-440q0 117 81.5 198.5T480-160q117 0 198.5-81.5T760-440h80q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-80ZM380-320v-60h120v-40H380v-140h180v60H440v40h80q17 0 28.5 11.5T560-420v60q0 17-11.5 28.5T520-320H380Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/forward_5.svg b/app/javascript/material-icons/400-24px/forward_5.svg
new file mode 100644
index 0000000000..bc0119a640
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/forward_5.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-440q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-800h6l-62-62 56-58 160 160-160 160-56-58 62-62h-6q-117 0-198.5 81.5T200-440q0 117 81.5 198.5T480-160q117 0 198.5-81.5T760-440h80q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-80ZM380-320v-60h120v-40H380v-140h180v60H440v40h80q17 0 28.5 11.5T560-420v60q0 17-11.5 28.5T520-320H380Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/public-fill.svg b/app/javascript/material-icons/400-24px/public-fill.svg
index 1e9e79de4d..104f26e133 100644
--- a/app/javascript/material-icons/400-24px/public-fill.svg
+++ b/app/javascript/material-icons/400-24px/public-fill.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-40-82v-78q-33 0-56.5-23.5T360-320v-40L168-552q-3 18-5.5 36t-2.5 36q0 121 79.5 212T440-162Zm276-102q20-22 36-47.5t26.5-53q10.5-27.5 16-56.5t5.5-59q0-98-54.5-179T600-776v16q0 33-23.5 56.5T520-680h-80v80q0 17-11.5 28.5T400-560h-80v80h240q17 0 28.5 11.5T600-440v120h40q26 0 47 15.5t29 40.5Z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-40-82v-78q-33 0-56.5-23.5T360-320v-40L168-552q-3 18-5.5 36t-2.5 36q0 121 79.5 212T440-162Zm276-102q41-45 62.5-100.5T800-480q0-98-54.5-179T600-776v16q0 33-23.5 56.5T520-680h-80v80q0 17-11.5 28.5T400-560h-80v80h240q17 0 28.5 11.5T600-440v120h40q26 0 47 15.5t29 40.5Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/public.svg b/app/javascript/material-icons/400-24px/public.svg
index 1e9e79de4d..104f26e133 100644
--- a/app/javascript/material-icons/400-24px/public.svg
+++ b/app/javascript/material-icons/400-24px/public.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-40-82v-78q-33 0-56.5-23.5T360-320v-40L168-552q-3 18-5.5 36t-2.5 36q0 121 79.5 212T440-162Zm276-102q20-22 36-47.5t26.5-53q10.5-27.5 16-56.5t5.5-59q0-98-54.5-179T600-776v16q0 33-23.5 56.5T520-680h-80v80q0 17-11.5 28.5T400-560h-80v80h240q17 0 28.5 11.5T600-440v120h40q26 0 47 15.5t29 40.5Z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-40-82v-78q-33 0-56.5-23.5T360-320v-40L168-552q-3 18-5.5 36t-2.5 36q0 121 79.5 212T440-162Zm276-102q41-45 62.5-100.5T800-480q0-98-54.5-179T600-776v16q0 33-23.5 56.5T520-680h-80v80q0 17-11.5 28.5T400-560h-80v80h240q17 0 28.5 11.5T600-440v120h40q26 0 47 15.5t29 40.5Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/replay_5-fill.svg b/app/javascript/material-icons/400-24px/replay_5-fill.svg
new file mode 100644
index 0000000000..c0c259829e
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/replay_5-fill.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-440h80q0 117 81.5 198.5T480-160q117 0 198.5-81.5T760-440q0-117-81.5-198.5T480-720h-6l62 62-56 58-160-160 160-160 56 58-62 62h6q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-440q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-80ZM380-320v-60h120v-40H380v-140h180v60H440v40h80q17 0 28.5 11.5T560-420v60q0 17-11.5 28.5T520-320H380Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/replay_5.svg b/app/javascript/material-icons/400-24px/replay_5.svg
new file mode 100644
index 0000000000..c0c259829e
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/replay_5.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-440h80q0 117 81.5 198.5T480-160q117 0 198.5-81.5T760-440q0-117-81.5-198.5T480-720h-6l62 62-56 58-160-160 160-160 56 58-62 62h6q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-440q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-80ZM380-320v-60h120v-40H380v-140h180v60H440v40h80q17 0 28.5 11.5T560-420v60q0 17-11.5 28.5T520-320H380Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/volume_down-fill.svg b/app/javascript/material-icons/400-24px/volume_down-fill.svg
new file mode 100644
index 0000000000..26ec2b09ab
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/volume_down-fill.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-360v-240h160l200-200v640L360-360H200Zm440 40v-322q45 21 72.5 65t27.5 97q0 53-27.5 96T640-320Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/volume_down.svg b/app/javascript/material-icons/400-24px/volume_down.svg
new file mode 100644
index 0000000000..a3fbc41002
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/volume_down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-360v-240h160l200-200v640L360-360H200Zm440 40v-322q45 21 72.5 65t27.5 97q0 53-27.5 96T640-320ZM480-606l-86 86H280v80h114l86 86v-252ZM380-480Z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index 109b69bca5..b328d8ee34 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -1,25 +1,27 @@
-@import 'mastodon/mixins';
-@import 'mastodon/variables';
-@import 'fonts/roboto';
-@import 'fonts/roboto-mono';
+@use 'mastodon/functions';
+@use 'mastodon/mixins';
+@use 'mastodon/variables';
+@use 'mastodon/css_variables';
+@use 'fonts/roboto';
+@use 'fonts/roboto-mono';
 
-@import 'mastodon/reset';
-@import 'mastodon/basics';
-@import 'mastodon/branding';
-@import 'mastodon/containers';
-@import 'mastodon/lists';
-@import 'mastodon/widgets';
-@import 'mastodon/forms';
-@import 'mastodon/accounts';
-@import 'mastodon/components';
-@import 'mastodon/polls';
-@import 'mastodon/modal';
-@import 'mastodon/emoji_picker';
-@import 'mastodon/annual_reports';
-@import 'mastodon/about';
-@import 'mastodon/tables';
-@import 'mastodon/admin';
-@import 'mastodon/dashboard';
-@import 'mastodon/rtl';
-@import 'mastodon/accessibility';
-@import 'mastodon/rich_text';
+@use 'mastodon/reset';
+@use 'mastodon/basics';
+@use 'mastodon/branding';
+@use 'mastodon/containers';
+@use 'mastodon/lists';
+@use 'mastodon/widgets';
+@use 'mastodon/forms';
+@use 'mastodon/accounts';
+@use 'mastodon/components';
+@use 'mastodon/polls';
+@use 'mastodon/modal';
+@use 'mastodon/emoji_picker';
+@use 'mastodon/annual_reports';
+@use 'mastodon/about';
+@use 'mastodon/tables';
+@use 'mastodon/admin';
+@use 'mastodon/dashboard';
+@use 'mastodon/rtl';
+@use 'mastodon/accessibility';
+@use 'mastodon/rich_text';
diff --git a/app/javascript/styles/contrast.scss b/app/javascript/styles/contrast.scss
index 5b43aecbe7..367be051f1 100644
--- a/app/javascript/styles/contrast.scss
+++ b/app/javascript/styles/contrast.scss
@@ -1,3 +1,3 @@
-@import 'contrast/variables';
-@import 'application';
-@import 'contrast/diff';
+@use 'contrast/variables';
+@use 'application';
+@use 'contrast/diff';
diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss
index ae607f484a..8aa05dd8ef 100644
--- a/app/javascript/styles/contrast/diff.scss
+++ b/app/javascript/styles/contrast/diff.scss
@@ -1,3 +1,5 @@
+@use '../mastodon/variables' as *;
+
 .status__content a,
 .reply-indicator__content a,
 .edit-indicator__content a,
diff --git a/app/javascript/styles/contrast/variables.scss b/app/javascript/styles/contrast/variables.scss
index 2bee5eca74..d63512ce43 100644
--- a/app/javascript/styles/contrast/variables.scss
+++ b/app/javascript/styles/contrast/variables.scss
@@ -1,3 +1,5 @@
+@use '../mastodon/functions' as *;
+
 // Dependent colors
 $black: #000000;
 
@@ -14,12 +16,13 @@ $ui-primary-color: $classic-primary-color !default;
 $ui-secondary-color: $classic-secondary-color !default;
 $ui-highlight-color: $classic-highlight-color !default;
 
-$darker-text-color: lighten($ui-primary-color, 20%) !default;
-$dark-text-color: lighten($ui-primary-color, 12%) !default;
-$secondary-text-color: lighten($ui-secondary-color, 6%) !default;
-$highlight-text-color: lighten($ui-highlight-color, 10%) !default;
-$action-button-color: lighten($ui-base-color, 50%);
-
-$inverted-text-color: $black !default;
-$lighter-text-color: darken($ui-base-color, 6%) !default;
-$light-text-color: darken($ui-primary-color, 40%) !default;
+@use '../mastodon/variables' with (
+  $darker-text-color: lighten($ui-primary-color, 20%),
+  $dark-text-color: lighten($ui-primary-color, 12%),
+  $secondary-text-color: lighten($ui-secondary-color, 6%),
+  $highlight-text-color: lighten($ui-highlight-color, 10%),
+  $action-button-color: lighten($ui-base-color, 50%),
+  $inverted-text-color: $black,
+  $lighter-text-color: darken($ui-base-color, 6%),
+  $light-text-color: darken($ui-primary-color, 40%)
+);
diff --git a/app/javascript/styles/full-dark.scss b/app/javascript/styles/full-dark.scss
index 105964ba6f..33195d5c6e 100644
--- a/app/javascript/styles/full-dark.scss
+++ b/app/javascript/styles/full-dark.scss
@@ -1,3 +1,4 @@
-@import 'full-dark/variables';
-@import 'application';
-@import 'full-dark/diff';
+@use 'full-dark/variables';
+@use 'full-dark/css_variables';
+@use 'application';
+@use 'full-dark/diff';
diff --git a/app/javascript/styles/full-dark/css_variables.scss b/app/javascript/styles/full-dark/css_variables.scss
new file mode 100644
index 0000000000..56e63dd23b
--- /dev/null
+++ b/app/javascript/styles/full-dark/css_variables.scss
@@ -0,0 +1,4 @@
+@use 'sass:color';
+@use '../mastodon/variables' as *;
+@use 'variables' as *;
+@use '../mastodon/functions' as *;
diff --git a/app/javascript/styles/full-dark/diff.scss b/app/javascript/styles/full-dark/diff.scss
index 9483e7ecb6..727072dc39 100644
--- a/app/javascript/styles/full-dark/diff.scss
+++ b/app/javascript/styles/full-dark/diff.scss
@@ -1,3 +1,7 @@
+@use 'sass:color';
+@use '../mastodon/functions' as *;
+@use '../mastodon/variables' as *;
+
 input[type='text']:not(#cw-spoiler-input),
 input[type='search'],
 input[type='number'],
diff --git a/app/javascript/styles/full-dark/variables.scss b/app/javascript/styles/full-dark/variables.scss
index 6cedec7df9..1720d716fe 100644
--- a/app/javascript/styles/full-dark/variables.scss
+++ b/app/javascript/styles/full-dark/variables.scss
@@ -1,11 +1,14 @@
 $classic-base-color: #282c37; // Midnight Express
 $classic-secondary-color: #d9e1e8; // Pattens Blue
 
-// Variables for defaults in UI
-$simple-background-color: $classic-base-color !default;
+@use '../mastodon/variables' with (
+  // Variables for defaults in UI
+  $simple-background-color: $classic-base-color,
 
-// Tell UI to use selected colors
-$ui-base-lighter-color: #969fbc !default; // Lighter darkest
+  // Tell UI to use selected colors
+  $ui-base-lighter-color: #969fbc,
 
-// For texts on inverted backgrounds
-$inverted-text-color: $classic-secondary-color !default;
+  // Lighter darkest
+  // For texts on inverted backgrounds
+  $inverted-text-color: $classic-secondary-color
+);
diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss
index 1f3310877a..1e339b4313 100644
--- a/app/javascript/styles/mailer.scss
+++ b/app/javascript/styles/mailer.scss
@@ -1,4 +1,4 @@
-@import 'fonts/inter';
+@use 'fonts/inter';
 
 body {
   accent-color: #6364ff;
diff --git a/app/javascript/styles/mastodon-light.scss b/app/javascript/styles/mastodon-light.scss
index 756a12d868..b530616a3c 100644
--- a/app/javascript/styles/mastodon-light.scss
+++ b/app/javascript/styles/mastodon-light.scss
@@ -1,3 +1,4 @@
-@import 'mastodon-light/variables';
-@import 'application';
-@import 'mastodon-light/diff';
+@use 'mastodon-light/variables';
+@use 'mastodon-light/css_variables';
+@use 'application';
+@use 'mastodon-light/diff';
diff --git a/app/javascript/styles/mastodon-light/css_variables.scss b/app/javascript/styles/mastodon-light/css_variables.scss
new file mode 100644
index 0000000000..d9311da1b9
--- /dev/null
+++ b/app/javascript/styles/mastodon-light/css_variables.scss
@@ -0,0 +1,21 @@
+@use 'sass:color';
+@use '../mastodon/variables' as *;
+@use 'variables' as *;
+@use '../mastodon/functions' as *;
+
+body {
+  --dropdown-border-color: hsl(240deg, 25%, 88%);
+  --dropdown-background-color: #fff;
+  --modal-border-color: hsl(240deg, 25%, 88%);
+  --modal-background-color: var(--background-color-tint);
+  --background-border-color: hsl(240deg, 25%, 88%);
+  --background-color: #fff;
+  --background-color-tint: rgba(255, 255, 255, 80%);
+  --background-filter: blur(10px);
+  --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)};
+  --rich-text-container-color: rgba(255, 216, 231, 100%);
+  --rich-text-text-color: rgba(114, 47, 83, 100%);
+  --rich-text-decorations-color: rgba(255, 175, 212, 100%);
+  --input-placeholder-color: #{color.adjust($dark-text-color, $alpha: -0.5)};
+  --input-background-color: #{darken($ui-base-color, 10%)};
+}
diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index d23d789e46..8ca860a86d 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -1,5 +1,8 @@
 // Notes!
 // Sass color functions, "darken" and "lighten" are automatically replaced.
+@use 'sass:color';
+@use '../mastodon/functions' as *;
+@use '../mastodon/variables' as *;
 
 .simple_form .button.button-tertiary {
   color: $highlight-text-color;
@@ -152,8 +155,12 @@
 }
 
 .reactions-bar__item.active {
-  background-color: mix($white, $ui-highlight-color, 80%);
-  border-color: mix(lighten($ui-base-color, 8%), $ui-highlight-color, 80%);
+  background-color: color.mix($white, $ui-highlight-color, 80%);
+  border-color: color.mix(
+    lighten($ui-base-color, 8%),
+    $ui-highlight-color,
+    80%
+  );
 }
 
 .media-modal__overlay .picture-in-picture__footer {
@@ -242,7 +249,7 @@
 
 // Change the default colors used on some parts of the profile pages
 .activity-stream-tabs {
-  background: $account-background-color;
+  background: $white;
   border-bottom-color: lighten($ui-base-color, 8%);
 }
 
@@ -284,7 +291,7 @@
   }
 
   .entry {
-    background: $account-background-color;
+    background: $white;
 
     .detailed-status.light,
     .more.light,
diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss
index c47ff2792c..5a32e2058e 100644
--- a/app/javascript/styles/mastodon-light/variables.scss
+++ b/app/javascript/styles/mastodon-light/variables.scss
@@ -1,87 +1,49 @@
 @use 'sass:color';
 
-// Dependent colors
-$black: #000000;
-$white: #ffffff;
+@use '../mastodon/functions' with (
+  $darken-multiplier: 1,
+  $lighten-multiplier: -1
+);
 
-$classic-base-color: hsl(240deg, 16%, 19%);
-$classic-primary-color: hsl(240deg, 29%, 70%);
-$classic-secondary-color: hsl(255deg, 25%, 88%);
-$classic-highlight-color: hsl(240deg, 100%, 69%);
-
-$blurple-600: hsl(252deg, 59%, 51%); // Iris
-$blurple-500: hsl(240deg, 100%, 69%); // Brand purple
-$blurple-300: hsl(237deg, 92%, 75%); // Faded Blue
+$black: #000000; // Black
+$white: #ffffff; // White
+$blurple-500: #6364ff; // Brand purple
 $grey-600: hsl(240deg, 8%, 33%); // Trout
 $grey-100: hsl(240deg, 51%, 90%); // Topaz
 
-$emoji-reaction-color: #dfe5f5 !default;
-$emoji-reaction-selected-color: #9ac1f2 !default;
+$classic-base-color: hsl(240deg, 16%, 19%);
+$classic-secondary-color: hsl(255deg, 25%, 88%);
+$classic-highlight-color: $blurple-500;
 
-// Differences
-$success-green: lighten(hsl(138deg, 32%, 35%), 8%);
+@use '../mastodon/variables' with (
+  $success-green: color.adjust(
+      hsl(138deg, 32%, 35%),
+      $lightness: 8%,
+      $space: hsl
+    ),
+  $base-overlay-background: $white,
 
-$base-overlay-background: $white !default;
-$valid-value-color: $success-green !default;
+  $ui-base-color: $classic-secondary-color,
+  $ui-base-lighter-color: hsl(250deg, 24%, 75%),
+  $ui-secondary-color: $classic-base-color,
 
-$ui-base-color: $classic-secondary-color !default;
-$ui-base-lighter-color: hsl(250deg, 24%, 75%);
-$ui-primary-color: $classic-primary-color !default;
-$ui-secondary-color: $classic-base-color !default;
-$ui-highlight-color: $classic-highlight-color !default;
+  $ui-button-secondary-color: $grey-600,
+  $ui-button-secondary-border-color: $grey-600,
+  $ui-button-secondary-focus-color: $white,
+  $ui-button-tertiary-color: $blurple-500,
+  $ui-button-tertiary-border-color: $blurple-500,
 
-$ui-button-secondary-color: $grey-600 !default;
-$ui-button-secondary-border-color: $grey-600 !default;
-$ui-button-secondary-focus-color: $white !default;
+  $primary-text-color: $black,
+  $darker-text-color: $classic-base-color,
+  $lighter-text-color: $classic-base-color,
+  $highlight-text-color: $classic-highlight-color,
+  $dark-text-color: hsl(240deg, 16%, 32%),
+  $light-text-color: hsl(240deg, 16%, 32%),
+  $inverted-text-color: $black,
 
-$ui-button-tertiary-color: $blurple-500 !default;
-$ui-button-tertiary-border-color: $blurple-500 !default;
+  $action-button-color: hsl(240deg, 16%, 45%),
+  $emojis-requiring-inversion: 'chains',
 
-$primary-text-color: $black !default;
-$darker-text-color: $classic-base-color !default;
-$highlight-text-color: $ui-highlight-color !default;
-$dark-text-color: hsl(240deg, 16%, 32%);
-$action-button-color: hsl(240deg, 16%, 45%);
-
-$inverted-text-color: $black !default;
-$lighter-text-color: $classic-base-color !default;
-$light-text-color: hsl(240deg, 16%, 32%);
-
-// Newly added colors
-$account-background-color: $white !default;
-
-// Invert darkened and lightened colors
-@function darken($color, $amount) {
-  @return hsl(
-    hue($color),
-    color.channel($color, 'saturation', $space: hsl),
-    color.channel($color, 'lightness', $space: hsl) + $amount
-  );
-}
-
-@function lighten($color, $amount) {
-  @return hsl(
-    hue($color),
-    color.channel($color, 'saturation', $space: hsl),
-    color.channel($color, 'lightness', $space: hsl) - $amount
-  );
-}
-
-$emojis-requiring-inversion: 'chains';
-
-body {
-  --dropdown-border-color: hsl(240deg, 25%, 88%);
-  --dropdown-background-color: #fff;
-  --modal-border-color: hsl(240deg, 25%, 88%);
-  --modal-background-color: var(--background-color-tint);
-  --background-border-color: hsl(240deg, 25%, 88%);
-  --background-color: #fff;
-  --background-color-tint: rgba(255, 255, 255, 80%);
-  --background-filter: blur(10px);
-  --on-surface-color: #{transparentize($ui-base-color, 0.65)};
-  --rich-text-container-color: rgba(255, 216, 231, 100%);
-  --rich-text-text-color: rgba(114, 47, 83, 100%);
-  --rich-text-decorations-color: rgba(255, 175, 212, 100%);
-  --input-placeholder-color: #{transparentize($dark-text-color, 0.5)};
-  --input-background-color: #{darken($ui-base-color, 10%)};
-}
+  $emoji-reaction-color: #dfe5f5,
+  $emoji-reaction-selected-color: #9ac1f2
+);
diff --git a/app/javascript/styles/mastodon/_functions.scss b/app/javascript/styles/mastodon/_functions.scss
new file mode 100644
index 0000000000..7190a6233e
--- /dev/null
+++ b/app/javascript/styles/mastodon/_functions.scss
@@ -0,0 +1,21 @@
+@use 'sass:color';
+
+$darken-multiplier: -1 !default;
+$lighten-multiplier: 1 !default;
+
+// Invert darkened and lightened colors
+@function darken($color, $amount) {
+  @return color.adjust(
+    $color,
+    $lightness: $amount * $darken-multiplier,
+    $space: hsl
+  );
+}
+
+@function lighten($color, $amount) {
+  @return color.adjust(
+    $color,
+    $lightness: $amount * $lighten-multiplier,
+    $space: hsl
+  );
+}
diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss
index 2599cb0e05..b7d9203e3f 100644
--- a/app/javascript/styles/mastodon/_mixins.scss
+++ b/app/javascript/styles/mastodon/_mixins.scss
@@ -1,3 +1,5 @@
+@use 'variables' as *;
+
 @mixin search-input {
   outline: 0;
   box-sizing: border-box;
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/_variables.scss
similarity index 68%
rename from app/javascript/styles/mastodon/variables.scss
rename to app/javascript/styles/mastodon/_variables.scss
index 6b1057605d..ea2d216441 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/_variables.scss
@@ -1,4 +1,5 @@
 @use 'sass:color';
+@use 'functions' as *;
 
 // Commonly used web colors
 $black: #000000; // Black
@@ -101,43 +102,13 @@ $media-modal-media-max-height: 80%;
 
 $no-gap-breakpoint: 1175px;
 $mobile-breakpoint: 630px;
+$no-columns-breakpoint: 600px;
 
 $font-sans-serif: 'mastodon-font-sans-serif' !default;
 $font-display: 'mastodon-font-display' !default;
 $font-monospace: 'mastodon-font-monospace' !default;
 
-:root {
-  --dropdown-border-color: #{lighten($ui-base-color, 4%)};
-  --dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)};
-  --dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
-    0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
-  --modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)};
-  --modal-background-variant-color: #{rgba($ui-base-color, 0.7)};
-  --modal-border-color: #{lighten($ui-base-color, 4%)};
-  --background-border-color: #{lighten($ui-base-color, 4%)};
-  --background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
-  --background-color: #{darken($ui-base-color, 8%)};
-  --background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
-  --surface-background-color: #{darken($ui-base-color, 4%)};
-  --surface-variant-background-color: #{$ui-base-color};
-  --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
-  --on-surface-color: #{transparentize($ui-base-color, 0.5)};
-  --avatar-border-radius: 8px;
-  --content-font-size: 15px;
-  --content-emoji-size: 20px;
-  --content-line-height: 22px;
-  --detail-content-font-size: 19px;
-  --detail-content-emoji-size: 24px;
-  --detail-content-line-height: 24px;
-  --media-outline-color: #{rgba(#fcf8ff, 0.15)};
-  --overlay-icon-shadow: drop-shadow(0 0 8px #{rgba($base-shadow-color, 0.25)});
-  --error-background-color: #{darken($error-red, 16%)};
-  --error-active-background-color: #{darken($error-red, 12%)};
-  --on-error-color: #fff;
-  --rich-text-container-color: rgba(87, 24, 60, 100%);
-  --rich-text-text-color: rgba(255, 175, 212, 100%);
-  --rich-text-decorations-color: rgba(128, 58, 95, 100%);
-  --input-placeholder-color: #{$dark-text-color};
-  --input-background-color: var(--surface-variant-background-color);
-  --on-input-color: #{$secondary-text-color};
-}
+$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange'
+  'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign'
+  'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on'
+  'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 03e3ccd643..a310e2ffe7 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -1,3 +1,5 @@
+@use 'variables' as *;
+
 $maximum-width: 1235px;
 $fluid-breakpoint: $maximum-width + 20px;
 
diff --git a/app/javascript/styles/mastodon/accessibility.scss b/app/javascript/styles/mastodon/accessibility.scss
index deaa0afdac..7cd2d4eae3 100644
--- a/app/javascript/styles/mastodon/accessibility.scss
+++ b/app/javascript/styles/mastodon/accessibility.scss
@@ -1,7 +1,4 @@
-$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange'
-  'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign'
-  'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on'
-  'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;
+@use 'variables' as *;
 
 %emoji-color-inversion {
   filter: invert(1);
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 89778b9f3b..34d4e840ef 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -1,3 +1,6 @@
+@use 'variables' as *;
+@use 'functions' as *;
+
 .card {
   & > a {
     display: block;
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 8c6609b3d9..c3035f946f 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -1,4 +1,6 @@
 @use 'sass:math';
+@use 'functions' as *;
+@use 'variables' as *;
 
 $no-columns-breakpoint: 890px;
 $sidebar-width: 300px;
@@ -1996,6 +1998,11 @@ a.sparkline {
           line-height: 20px;
           font-weight: 600;
           margin-bottom: 16px;
+
+          a {
+            color: inherit;
+            text-decoration: none;
+          }
         }
       }
     }
diff --git a/app/javascript/styles/mastodon/annual_reports.scss b/app/javascript/styles/mastodon/annual_reports.scss
index dff1c76eca..96500a18bb 100644
--- a/app/javascript/styles/mastodon/annual_reports.scss
+++ b/app/javascript/styles/mastodon/annual_reports.scss
@@ -1,3 +1,5 @@
+@use 'variables' as *;
+
 :root {
   --indigo-1: #17063b;
   --indigo-2: #2f0c7a;
diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss
index b7140fa6d6..7ae8cb2d08 100644
--- a/app/javascript/styles/mastodon/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -1,3 +1,6 @@
+@use 'variables' as *;
+@use 'functions' as *;
+
 @function hex-color($color) {
   @if type-of($color) == 'color' {
     $color: str-slice(ie-hex-str($color), 4);
@@ -14,7 +17,12 @@ body {
   font-weight: 400;
   color: $primary-text-color;
   text-rendering: optimizelegibility;
-  font-feature-settings: 'kern';
+
+  // Disable kerning for Japanese text to preserve monospaced alignment for readability
+  &:not(:lang(ja)) {
+    font-feature-settings: 'kern';
+  }
+
   text-size-adjust: none;
   -webkit-tap-highlight-color: rgba(0, 0, 0, 0%);
   -webkit-tap-highlight-color: transparent;
@@ -92,6 +100,7 @@ body {
 
     &.with-modals--active {
       overflow-y: hidden;
+      overscroll-behavior: none;
     }
   }
 
diff --git a/app/javascript/styles/mastodon/branding.scss b/app/javascript/styles/mastodon/branding.scss
index d1bddc68b0..8e8dd3530b 100644
--- a/app/javascript/styles/mastodon/branding.scss
+++ b/app/javascript/styles/mastodon/branding.scss
@@ -1,3 +1,5 @@
+@use 'variables' as *;
+
 .logo {
   color: $primary-text-color;
 }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 8bce42f84b..9758ecc62c 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1,3 +1,8 @@
+@use 'sass:color';
+@use 'variables' as *;
+@use 'functions' as *;
+@use 'mixins' as *;
+
 .app-body {
   -webkit-overflow-scrolling: touch;
   -ms-overflow-style: -ms-autohiding-scrollbar;
@@ -1911,18 +1916,22 @@ body > [data-popper-placement] {
 .detailed-status__wrapper-direct {
   .detailed-status,
   .detailed-status__action-bar {
-    background: mix($ui-base-color, $ui-highlight-color, 95%);
+    background: color.mix($ui-base-color, $ui-highlight-color, 95%);
   }
 
   &:focus {
     .detailed-status,
     .detailed-status__action-bar {
-      background: mix(lighten($ui-base-color, 4%), $ui-highlight-color, 95%);
+      background: color.mix(
+        lighten($ui-base-color, 4%),
+        $ui-highlight-color,
+        95%
+      );
     }
   }
 
   .detailed-status__action-bar {
-    border-top-color: mix(
+    border-top-color: color.mix(
       lighten($ui-base-color, 8%),
       $ui-highlight-color,
       95%
@@ -1943,29 +1952,21 @@ body > [data-popper-placement] {
 }
 
 .domain {
-  padding: 10px;
+  padding: 16px;
   border-bottom: 1px solid var(--background-border-color);
+  display: flex;
+  align-items: center;
+  gap: 8px;
 
-  .domain__domain-name {
+  &__domain-name {
     flex: 1 1 auto;
-    display: block;
     color: $primary-text-color;
-    text-decoration: none;
-    font-size: 14px;
+    font-size: 15px;
+    line-height: 21px;
     font-weight: 500;
   }
 }
 
-.domain__wrapper {
-  display: flex;
-}
-
-.domain_buttons {
-  height: 18px;
-  padding: 10px;
-  white-space: nowrap;
-}
-
 .account {
   padding: 16px;
   border-bottom: 1px solid var(--background-border-color);
@@ -2528,49 +2529,6 @@ a.account__display-name {
   }
 }
 
-.image-loader {
-  position: relative;
-  width: 100%;
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-direction: column;
-  scrollbar-width: none; /* Firefox */
-  -ms-overflow-style: none; /* IE 10+ */
-
-  * {
-    scrollbar-width: none; /* Firefox */
-    -ms-overflow-style: none; /* IE 10+ */
-  }
-
-  &::-webkit-scrollbar,
-  *::-webkit-scrollbar {
-    width: 0;
-    height: 0;
-    background: transparent; /* Chrome/Safari/Webkit */
-  }
-
-  .image-loader__preview-canvas {
-    max-width: $media-modal-media-max-width;
-    max-height: $media-modal-media-max-height;
-    background: url('../images/void.png') repeat;
-    object-fit: contain;
-  }
-
-  .loading-bar__container {
-    position: relative;
-  }
-
-  .loading-bar {
-    position: absolute;
-  }
-
-  &.image-loader--amorphous .image-loader__preview-canvas {
-    display: none;
-  }
-}
-
 .zoomable-image {
   position: relative;
   width: 100%;
@@ -2578,13 +2536,61 @@ a.account__display-name {
   display: flex;
   align-items: center;
   justify-content: center;
+  scrollbar-width: none;
+  overflow: hidden;
+  user-select: none;
 
   img {
     max-width: $media-modal-media-max-width;
     max-height: $media-modal-media-max-height;
     width: auto;
     height: auto;
-    object-fit: contain;
+    outline: 1px solid var(--media-outline-color);
+    outline-offset: -1px;
+    border-radius: 8px;
+    touch-action: none;
+  }
+
+  &--zoomed-in {
+    z-index: 9999;
+    cursor: grab;
+
+    img {
+      outline: none;
+      border-radius: 0;
+    }
+  }
+
+  &--dragging {
+    cursor: grabbing;
+  }
+
+  &--error img {
+    visibility: hidden;
+  }
+
+  &__preview {
+    max-width: $media-modal-media-max-width;
+    max-height: $media-modal-media-max-height;
+    position: absolute;
+    z-index: 1;
+    outline: 1px solid var(--media-outline-color);
+    outline-offset: -1px;
+    border-radius: 8px;
+    overflow: hidden;
+
+    canvas {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      z-index: -1;
+    }
+  }
+
+  .loading-indicator {
+    z-index: 2;
+    mix-blend-mode: luminosity;
   }
 }
 
@@ -5576,6 +5582,9 @@ a.status-card {
 
     &__results {
       &__item {
+        display: flex;
+        align-items: center;
+        gap: 0.5em;
         cursor: pointer;
         color: $primary-text-color;
         font-size: 14px;
@@ -5887,6 +5896,7 @@ a.status-card {
   z-index: 9999;
   pointer-events: none;
   user-select: none;
+  overscroll-behavior: none;
 }
 
 .modal-root__modal {
@@ -6020,7 +6030,7 @@ a.status-card {
   .picture-in-picture__footer {
     border-radius: 0;
     background: transparent;
-    padding: 20px 0;
+    padding: 16px;
 
     .icon-button {
       color: $white;
@@ -6579,7 +6589,7 @@ a.status-card {
 
     a {
       text-decoration: none;
-      color: $inverted-text-color;
+      color: $highlight-text-color;
       font-weight: 500;
 
       &:hover {
@@ -7235,6 +7245,10 @@ a.status-card {
       filter: var(--overlay-icon-shadow);
     }
   }
+
+  &--error img {
+    visibility: hidden;
+  }
 }
 
 .media-gallery__item-thumbnail {
@@ -7389,6 +7403,8 @@ a.status-card {
     height: 100% !important;
     margin: 0;
     aspect-ratio: auto !important;
+    outline: none;
+    border-radius: 0;
 
     video {
       width: 100% !important;
@@ -7420,6 +7436,15 @@ a.status-card {
     }
   }
 
+  .media-gallery__actions {
+    opacity: 0;
+    transition: opacity 0.1s ease;
+
+    &.active {
+      opacity: 1;
+    }
+  }
+
   &.inactive {
     video,
     .video-player__controls {
@@ -7570,7 +7595,7 @@ a.status-card {
       inset-inline-start: 0;
       top: 50%;
       transform: translate(0, -50%);
-      background: lighten($ui-highlight-color, 8%);
+      background: $white;
     }
 
     &__handle {
@@ -7583,7 +7608,7 @@ a.status-card {
       inset-inline-start: 0;
       margin-inline-start: -6px;
       transform: translate(0, -50%);
-      background: lighten($ui-highlight-color, 8%);
+      background: $white;
       box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
       opacity: 0;
 
@@ -7637,7 +7662,7 @@ a.status-card {
       height: 4px;
       border-radius: 4px;
       top: 14px;
-      background: lighten($ui-highlight-color, 8%);
+      background: $white;
     }
 
     &__buffer {
@@ -7653,7 +7678,7 @@ a.status-card {
       height: 12px;
       top: 10px;
       margin-inline-start: -6px;
-      background: lighten($ui-highlight-color, 8%);
+      background: $white;
       box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
 
       .no-reduce-motion & {
@@ -7662,6 +7687,7 @@ a.status-card {
 
       &.active {
         opacity: 1;
+        cursor: grabbing;
       }
     }
 
@@ -7672,6 +7698,28 @@ a.status-card {
     }
   }
 
+  &__hotkey-indicator {
+    position: absolute;
+    top: 50%;
+    inset-inline-start: 50%;
+    transform: translate(-50%, -50%);
+    background: rgba($base-shadow-color, 0.45);
+    backdrop-filter: var(--background-filter);
+    color: $white;
+    border-radius: 8px;
+    padding: 16px 24px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+
+    &__label {
+      font-size: 15px;
+      font-weight: 500;
+    }
+  }
+
   &.detailed,
   &.fullscreen {
     .video-player__buttons {
@@ -7731,7 +7779,8 @@ a.status-card {
     border-radius: 0;
   }
 
-  .load-more {
+  .load-more,
+  .timeline-hint {
     grid-column: span 3;
   }
 }
@@ -7856,14 +7905,11 @@ a.status-card {
 }
 
 .radio-button__input.checked::before {
-  position: absolute;
-  left: 2px;
-  top: 2px;
   content: '';
   display: block;
   border-radius: 50%;
-  width: 12px;
-  height: 12px;
+  width: calc(100% - 4px);
+  height: calc(100% - 4px);
   background: $ui-highlight-color;
 }
 
@@ -8528,13 +8574,9 @@ noscript {
   &__item {
     display: flex;
     align-items: center;
-    padding: 15px;
+    padding: 16px;
     border-bottom: 1px solid var(--background-border-color);
-    gap: 15px;
-
-    &:last-child {
-      border-bottom: 0;
-    }
+    gap: 8px;
 
     &__name {
       flex: 1 1 auto;
@@ -8641,7 +8683,7 @@ noscript {
   }
 
   &--compact &__item {
-    padding: 10px;
+    padding: 12px;
   }
 }
 
@@ -8857,7 +8899,7 @@ noscript {
     &.active {
       transition: all 100ms ease-in;
       transition-property: background-color, color;
-      background-color: mix(
+      background-color: color.mix(
         lighten($ui-base-color, 12%),
         $ui-highlight-color,
         80%
@@ -9758,6 +9800,7 @@ noscript {
   border: 1px solid $highlight-text-color;
   background: rgba($highlight-text-color, 0.15);
   overflow: hidden;
+  flex-shrink: 0;
 
   &__background-image {
     width: 125%;
@@ -10185,6 +10228,9 @@ noscript {
 }
 
 .notification-bar-action {
+  display: inline-block;
+  border: 0;
+  background: transparent;
   text-transform: uppercase;
   margin-inline-start: 10px;
   cursor: pointer;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index ac1f862a09..7db9ca409d 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -1,3 +1,5 @@
+@use 'variables' as *;
+
 .container-alt {
   width: 700px;
   margin: 0 auto;
diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss
new file mode 100644
index 0000000000..d1a357f730
--- /dev/null
+++ b/app/javascript/styles/mastodon/css_variables.scss
@@ -0,0 +1,39 @@
+@use 'sass:color';
+@use 'functions' as *;
+@use 'variables' as *;
+
+:root {
+  --dropdown-border-color: #{lighten($ui-base-color, 4%)};
+  --dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)};
+  --dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
+    0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
+  --modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)};
+  --modal-background-variant-color: #{rgba($ui-base-color, 0.7)};
+  --modal-border-color: #{lighten($ui-base-color, 4%)};
+  --background-border-color: #{lighten($ui-base-color, 4%)};
+  --background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
+  --background-color: #{darken($ui-base-color, 8%)};
+  --background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
+  --surface-background-color: #{darken($ui-base-color, 4%)};
+  --surface-variant-background-color: #{$ui-base-color};
+  --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
+  --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)};
+  --avatar-border-radius: 8px;
+  --media-outline-color: #{rgba(#fcf8ff, 0.15)};
+  --overlay-icon-shadow: drop-shadow(0 0 8px #{rgba($base-shadow-color, 0.25)});
+  --error-background-color: #{darken($error-red, 16%)};
+  --error-active-background-color: #{darken($error-red, 12%)};
+  --on-error-color: #fff;
+  --rich-text-container-color: rgba(87, 24, 60, 100%);
+  --rich-text-text-color: rgba(255, 175, 212, 100%);
+  --rich-text-decorations-color: rgba(128, 58, 95, 100%);
+  --input-placeholder-color: #{$dark-text-color};
+  --input-background-color: var(--surface-variant-background-color);
+  --on-input-color: #{$secondary-text-color};
+  --content-font-size: 15px;
+  --content-emoji-size: 20px;
+  --content-line-height: 22px;
+  --detail-content-font-size: 19px;
+  --detail-content-emoji-size: 24px;
+  --detail-content-line-height: 24px;
+}
diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss
index d049b2456c..c99cdc357a 100644
--- a/app/javascript/styles/mastodon/dashboard.scss
+++ b/app/javascript/styles/mastodon/dashboard.scss
@@ -1,3 +1,6 @@
+@use 'functions' as *;
+@use 'variables' as *;
+
 .dashboard__counters {
   display: flex;
   flex-wrap: wrap;
diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index e883bb4ab5..68f4c87ecd 100644
--- a/app/javascript/styles/mastodon/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
@@ -1,3 +1,6 @@
+@use 'variables' as *;
+@use 'functions' as *;
+
 .emoji-mart {
   font-size: 13px;
   display: inline-block;
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index ea137d1d01..f8eaa43a20 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -1,4 +1,5 @@
-$no-columns-breakpoint: 600px;
+@use 'variables' as *;
+@use 'functions' as *;
 
 code {
   font-family: $font-monospace, monospace;
@@ -352,10 +353,33 @@ code {
     columns: unset;
   }
 
-  .input.datetime .label_input select {
-    display: inline-block;
-    width: auto;
-    flex: 0;
+  .input.datetime .label_input,
+  .input.date .label_input {
+    display: flex;
+    gap: 4px;
+    align-items: center;
+
+    select {
+      display: inline-block;
+      width: auto;
+      flex: 0;
+    }
+  }
+
+  .input.date_of_birth .label_input {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+
+    input {
+      box-sizing: content-box;
+      width: 32px;
+      flex: 0;
+
+      &:last-child {
+        width: 64px;
+      }
+    }
   }
 
   .input.select.select--languages {
diff --git a/app/javascript/styles/mastodon/modal.scss b/app/javascript/styles/mastodon/modal.scss
index 60e7d62245..7d060a2681 100644
--- a/app/javascript/styles/mastodon/modal.scss
+++ b/app/javascript/styles/mastodon/modal.scss
@@ -1,3 +1,6 @@
+@use 'variables' as *;
+@use 'functions' as *;
+
 .modal-layout {
   background: darken($ui-base-color, 4%)
     url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}33"/></svg>')
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index ced4c60c44..f49ce3c413 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -1,3 +1,6 @@
+@use 'variables' as *;
+@use 'functions' as *;
+
 .poll {
   margin-top: 16px;
   font-size: 14px;
diff --git a/app/javascript/styles/mastodon/reset.scss b/app/javascript/styles/mastodon/reset.scss
index d1ca4a1837..2dce637a06 100644
--- a/app/javascript/styles/mastodon/reset.scss
+++ b/app/javascript/styles/mastodon/reset.scss
@@ -1,3 +1,5 @@
+@use 'variables' as *;
+
 /* http://meyerweb.com/eric/tools/css/reset/
    v2.0 | 20110126
    License: none (public domain)
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 0a05ce7c62..6aa94a97bc 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -1,3 +1,6 @@
+@use 'functions' as *;
+@use 'variables' as *;
+
 body.rtl {
   direction: rtl;
 
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 310d3def07..620518ebf8 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -1,3 +1,6 @@
+@use 'variables' as *;
+@use 'functions' as *;
+
 .table {
   width: 100%;
   max-width: 100%;
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index f467069052..8d09c7d583 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -1,3 +1,6 @@
+@use 'variables' as *;
+@use 'functions' as *;
+
 .directory {
   &__tag {
     box-sizing: border-box;
diff --git a/app/lib/account_reach_finder.rb b/app/lib/account_reach_finder.rb
index 19464024a6..4bf5c229a5 100644
--- a/app/lib/account_reach_finder.rb
+++ b/app/lib/account_reach_finder.rb
@@ -10,7 +10,7 @@ class AccountReachFinder
   end
 
   def inboxes
-    (followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + relay_inboxes).uniq
+    (followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + recently_followed_inboxes + recently_requested_inboxes + relay_inboxes).uniq
   end
 
   private
@@ -31,13 +31,32 @@ class AccountReachFinder
       .take(RECENT_LIMIT)
   end
 
+  def recently_followed_inboxes
+    @account
+      .following
+      .where(follows: { created_at: recent_date_cutoff... })
+      .inboxes
+      .take(RECENT_LIMIT)
+  end
+
+  def recently_requested_inboxes
+    Account
+      .where(id: @account.follow_requests.where({ created_at: recent_date_cutoff... }).select(:target_account_id))
+      .inboxes
+      .take(RECENT_LIMIT)
+  end
+
   def relay_inboxes
     Relay.enabled.pluck(:inbox_url)
   end
 
   def oldest_status_id
     Mastodon::Snowflake
-      .id_at(STATUS_SINCE.ago, with_random: false)
+      .id_at(recent_date_cutoff, with_random: false)
+  end
+
+  def recent_date_cutoff
+    @account.suspended? && @account.suspension_origin_local? ? @account.suspended_at - STATUS_SINCE : STATUS_SINCE.ago
   end
 
   def recent_statuses
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index ed679ac4b5..d726f5e68c 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -130,12 +130,7 @@ class ActivityPub::Activity
 
   def first_mentioned_local_account
     audience = (as_array(@json['to']) + as_array(@json['cc'])).map { |x| value_or_id(x) }.uniq
-    local_usernames = audience.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }
-                              .map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
-
-    return if local_usernames.empty?
-
-    Account.local.where(username: local_usernames).first
+    ActivityPub::TagManager.instance.uris_to_local_accounts(audience).first
   end
 
   def first_local_follower
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 50e5bf001d..6cdc21ce75 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -4,6 +4,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   include FormattingHelper
   include NgRuleHelper
 
+  LINK_MEDIA_TYPES = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].freeze
+
   def perform
     @account.schedule_refresh_if_stale!
 
@@ -433,7 +435,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     collection = @object['replies']
     return if collection.blank?
 
-    replies = ActivityPub::FetchRepliesService.new.call(status, collection, allow_synchronous_requests: false, request_id: @options[:request_id])
+    replies = ActivityPub::FetchRepliesService.new.call(status.account.uri, collection, allow_synchronous_requests: false, request_id: @options[:request_id])
     return unless replies.nil?
 
     uri = value_or_id(collection)
@@ -563,11 +565,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   def addresses_local_accounts?
     return true if @options[:delivered_to_account_id]
 
-    local_usernames = (audience_to + audience_cc).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
-
-    return false if local_usernames.empty?
-
-    Account.local.exists?(username: local_usernames)
+    ActivityPub::TagManager.instance.uris_to_local_accounts((audience_to + audience_cc).uniq).exists?
   end
 
   def tombstone_exists?
@@ -645,8 +643,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     @quote ||= quote_from_tags || @object['quote'] || @object['quoteUrl'] || @object['quoteURL'] || @object['_misskey_quote']
   end
 
-  LINK_MEDIA_TYPES = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].freeze
-
   def quote_from_tags
     return @quote_from_tags if defined?(@quote_from_tags)
 
diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb
index 89e94d168b..1968f18468 100644
--- a/app/lib/activitypub/parser/status_parser.rb
+++ b/app/lib/activitypub/parser/status_parser.rb
@@ -5,6 +5,9 @@ class ActivityPub::Parser::StatusParser
 
   NORMALIZED_LOCALE_NAMES = LanguagesHelper::SUPPORTED_LOCALES.keys.index_by(&:downcase).freeze
 
+  SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
+  SCAN_SEARCHABILITY_FEDIBIRD_RE = /searchable_by_(all_users|followers_only|reacted_users_only|nobody)/
+
   # @param [Hash] json
   # @param [Hash] options
   # @option options [String] :followers_collection
@@ -182,9 +185,6 @@ class ActivityPub::Parser::StatusParser
     %i(public unlisted).include?(visibility) ? :public : :limited
   end
 
-  SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
-  SCAN_SEARCHABILITY_FEDIBIRD_RE = /searchable_by_(all_users|followers_only|reacted_users_only|nobody)/
-
   def default_searchability_from_bio?
     note = @account.note
     return false if note.blank?
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index d84fbd77ca..3ead162ec3 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -242,6 +242,19 @@ class ActivityPub::TagManager
     path_params[param]
   end
 
+  def uris_to_local_accounts(uris)
+    usernames = []
+    ids = []
+
+    uris.each do |uri|
+      param, value = uri_to_local_account_params(uri)
+      usernames << value.downcase if param == :username
+      ids << value if param == :id
+    end
+
+    Account.local.with_username(usernames).or(Account.local.where(id: ids))
+  end
+
   def uri_to_actor(uri)
     uri_to_resource(uri, Account)
   end
@@ -252,7 +265,7 @@ class ActivityPub::TagManager
     if local_uri?(uri)
       case klass.name
       when 'Account'
-        klass.find_local(uri_to_local_id(uri, :username))
+        uris_to_local_accounts([uri]).first
       else
         StatusFinder.new(uri).status
       end
@@ -334,4 +347,20 @@ class ActivityPub::TagManager
       end
     end
   end
+
+  private
+
+  def uri_to_local_account_params(uri)
+    return unless local_uri?(uri)
+
+    path_params = Rails.application.routes.recognize_path(uri)
+
+    # TODO: handle numeric IDs
+    case path_params[:controller]
+    when 'accounts'
+      [:username, path_params[:username]]
+    when 'instance_actors'
+      [:id, -99]
+    end
+  end
 end
diff --git a/app/lib/admin/metrics/dimension/space_usage_dimension.rb b/app/lib/admin/metrics/dimension/space_usage_dimension.rb
index f1b6dba040..0d3fd8db33 100644
--- a/app/lib/admin/metrics/dimension/space_usage_dimension.rb
+++ b/app/lib/admin/metrics/dimension/space_usage_dimension.rb
@@ -45,7 +45,6 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
       PreviewCard.sum(:image_file_size),
       Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
       Backup.sum(:dump_file_size),
-      Import.sum(:data_file_size),
       SiteUpload.sum(:file_file_size),
     ].sum
 
diff --git a/app/lib/admin/system_check/media_privacy_check.rb b/app/lib/admin/system_check/media_privacy_check.rb
index 2ddc8e8b07..378a8ce294 100644
--- a/app/lib/admin/system_check/media_privacy_check.rb
+++ b/app/lib/admin/system_check/media_privacy_check.rb
@@ -76,7 +76,7 @@ class Admin::SystemCheck::MediaPrivacyCheck < Admin::SystemCheck::BaseCheck
 
   def media_attachment
     @media_attachment ||= begin
-      attachment = Account.representative.media_attachments.first
+      attachment = Account.representative.media_attachments.take
       if attachment.present?
         attachment.touch
         attachment
diff --git a/app/lib/content_security_policy.rb b/app/lib/content_security_policy.rb
index c764d1856d..fc42e2d48b 100644
--- a/app/lib/content_security_policy.rb
+++ b/app/lib/content_security_policy.rb
@@ -10,7 +10,7 @@ class ContentSecurityPolicy
   end
 
   def media_hosts
-    [assets_host, cdn_host_value, paperclip_root_url].compact
+    [assets_host, cdn_host_value, paperclip_root_url].concat(extra_media_hosts).compact
   end
 
   def sso_host
@@ -31,6 +31,10 @@ class ContentSecurityPolicy
 
   private
 
+  def extra_media_hosts
+    ENV.fetch('EXTRA_MEDIA_HOSTS', '').split(/(?:\s*,\s*|\s+)/)
+  end
+
   def url_from_configured_asset_host
     Rails.configuration.action_controller.asset_host
   end
diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb
index c0302767ef..1574d4588d 100644
--- a/app/lib/emoji_formatter.rb
+++ b/app/lib/emoji_formatter.rb
@@ -24,7 +24,15 @@ class EmojiFormatter
   def to_s
     return html if custom_emojis.empty? || html.blank?
 
-    tree = Nokogiri::HTML5.fragment(html)
+    begin
+      tree = Nokogiri::HTML5.fragment(html)
+    rescue ArgumentError
+      # This can happen if one of the Nokogumbo limits is encountered
+      # Unfortunately, it does not use a more precise error class
+      # nor allows more graceful handling
+      return ''
+    end
+
     tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node|
       i                     = -1
       inside_shortname      = false
diff --git a/app/lib/fasp/request.rb b/app/lib/fasp/request.rb
new file mode 100644
index 0000000000..2addbe8502
--- /dev/null
+++ b/app/lib/fasp/request.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+class Fasp::Request
+  def initialize(provider)
+    @provider = provider
+  end
+
+  def get(path)
+    perform_request(:get, path)
+  end
+
+  def post(path, body: nil)
+    perform_request(:post, path, body:)
+  end
+
+  def delete(path, body: nil)
+    perform_request(:delete, path, body:)
+  end
+
+  private
+
+  def perform_request(verb, path, body: nil)
+    url = @provider.url(path)
+    body = body.present? ? body.to_json : ''
+    headers = request_headers(verb, url, body)
+    response = HTTP.headers(headers).send(verb, url, body:)
+    validate!(response)
+
+    response.parse if response.body.present?
+  end
+
+  def request_headers(verb, url, body = '')
+    result = {
+      'accept' => 'application/json',
+      'content-digest' => content_digest(body),
+    }
+    result.merge(signature_headers(verb, url, result))
+  end
+
+  def content_digest(body)
+    "sha-256=:#{OpenSSL::Digest.base64digest('sha256', body || '')}:"
+  end
+
+  def signature_headers(verb, url, headers)
+    linzer_request = Linzer.new_request(verb, url, {}, headers)
+    message = Linzer::Message.new(linzer_request)
+    key = Linzer.new_ed25519_key(@provider.server_private_key_pem, @provider.remote_identifier)
+    signature = Linzer.sign(key, message, %w(@method @target-uri content-digest))
+    Linzer::Signer.send(:populate_parameters, key, {})
+
+    signature.to_h
+  end
+
+  def validate!(response)
+    content_digest_header = response.headers['content-digest']
+    raise Mastodon::SignatureVerificationError, 'content-digest missing' if content_digest_header.blank?
+    raise Mastodon::SignatureVerificationError, 'content-digest does not match' if content_digest_header != content_digest(response.body)
+
+    signature_input = response.headers['signature-input']&.encode('UTF-8')
+    raise Mastodon::SignatureVerificationError, 'signature-input is missing' if signature_input.blank?
+
+    linzer_response = Linzer.new_response(
+      response.body,
+      response.status,
+      {
+        'content-digest' => content_digest_header,
+        'signature-input' => signature_input,
+        'signature' => response.headers['signature'],
+      }
+    )
+    message = Linzer::Message.new(linzer_response)
+    key = Linzer.new_ed25519_public_key(@provider.provider_public_key_pem)
+    signature = Linzer::Signature.build(message.headers)
+    Linzer.verify(key, message, signature)
+  end
+end
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index df4fc17908..54cb464463 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -32,6 +32,15 @@ class FeedManager
     "feed:#{type}:#{id}:#{subtype}"
   end
 
+  # The number of items in the given timeline
+  # @param [Symbol] type
+  # @param [Integer] id
+  # @param [Symbol] subtype
+  # @return [Integer]
+  def timeline_size(type, id, subtype = nil)
+    redis.zcard(key(type, id, subtype))
+  end
+
   # The filter result of the status to a particular feed
   # @param [Symbol] timeline_type
   # @param [Status] status
@@ -42,7 +51,7 @@ class FeedManager
     when :home
       filter_from_home(status, receiver.id, build_crutches(receiver.id, [status]), :home)
     when :list
-      (filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list, stl_home: stl_home)
+      (filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status], list: receiver, stl_home: stl_home), :list, stl_home: stl_home)
     when :mentions
       filter_from_mentions?(status, receiver.id) ? :filter : nil
     when :tags
@@ -136,7 +145,7 @@ class FeedManager
 
     timeline_key = key(:home, into_account.id)
     aggregate    = into_account.user&.aggregates_reblogs?
-    query        = from_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
+    query        = from_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
 
     if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
       oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
@@ -164,7 +173,7 @@ class FeedManager
 
     timeline_key = key(:list, list.id)
     aggregate    = list.account.user&.aggregates_reblogs?
-    query        = from_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
+    query        = from_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
 
     if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
       oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
@@ -172,10 +181,10 @@ class FeedManager
     end
 
     statuses = query.to_a
-    crutches = build_crutches(list.account_id, statuses)
+    crutches = build_crutches(list.account_id, statuses, list: list)
 
     statuses.each do |status|
-      next if filter_from_home(status, list.account_id, crutches) || filter_from_list?(status, list)
+      next if filter_from_home(status, list.account_id, crutches, :list)
 
       add_to_feed(:list, list.id, status, aggregate_reblogs: aggregate)
     end
@@ -309,23 +318,32 @@ class FeedManager
     limit        = FeedManager::MAX_ITEMS / 2
     aggregate    = account.user&.aggregates_reblogs?
     timeline_key = key(:home, account.id)
+    over_limit = false
 
     account.statuses.limit(limit).each do |status|
       add_to_feed(:home, account.id, status, aggregate_reblogs: aggregate)
     end
 
     account.following.includes(:account_stat).reorder(nil).find_each do |target_account|
-      if redis.zcard(timeline_key) >= limit
+      query = target_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(limit)
+
+      over_limit ||= redis.zcard(timeline_key) >= limit
+      if over_limit
         oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
-        last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at)
+        last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at, with_random: false)
 
         # If the feed is full and this account has not posted more recently
         # than the last item on the feed, then we can skip the whole account
         # because none of its statuses would stay on the feed anyway
         next if last_status_score < oldest_home_score
+
+        # No need to get older statuses
+        query = query.where(id: oldest_home_score...)
       end
 
-      statuses = target_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit)
+      statuses = query.to_a
+      next if statuses.empty?
+
       crutches = build_crutches(account.id, statuses)
 
       statuses.each do |status|
@@ -345,23 +363,32 @@ class FeedManager
     limit        = FeedManager::MAX_ITEMS / 2
     aggregate    = list.account.user&.aggregates_reblogs?
     timeline_key = key(:list, list.id)
+    over_limit = false
 
     list.active_accounts.includes(:account_stat).reorder(nil).find_each do |target_account|
-      if redis.zcard(timeline_key) >= limit
+      query = target_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(limit)
+
+      over_limit ||= redis.zcard(timeline_key) >= limit
+      if over_limit
         oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
-        last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at)
+        last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at, with_random: false)
 
         # If the feed is full and this account has not posted more recently
         # than the last item on the feed, then we can skip the whole account
         # because none of its statuses would stay on the feed anyway
         next if last_status_score < oldest_home_score
+
+        # No need to get older statuses
+        query = query.where(id: oldest_home_score...)
       end
 
-      statuses = target_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit)
-      crutches = build_crutches(list.account_id, statuses)
+      statuses = query.to_a
+      next if statuses.empty?
+
+      crutches = build_crutches(list.account_id, statuses, list: list)
 
       statuses.each do |status|
-        next if filter_from_home(status, list.account_id, crutches) || filter_from_list?(status, list)
+        next if filter_from_home(status, list.account_id, crutches, :list)
 
         add_to_feed(:list, list.id, status, aggregate_reblogs: aggregate)
       end
@@ -632,8 +659,9 @@ class FeedManager
   # are going to be checked by the filtering methods
   # @param [Integer] receiver_id
   # @param [Array<Status>] statuses
+  # @param [List] list
   # @return [Hash]
-  def build_crutches(receiver_id, statuses) # rubocop:disable Metrics/AbcSize
+  def build_crutches(receiver_id, statuses, list: nil, stl_home: false)
     crutches = {}
 
     crutches[:active_mentions] = crutches_active_mentions(statuses)
@@ -650,25 +678,43 @@ class FeedManager
       arr
     end
 
-    lists = List.where(account_id: receiver_id, exclusive: true)
-    antennas = Antenna.where(list: lists, insert_feeds: true)
-
-    replied_accounts  = statuses.filter_map(&:in_reply_to_account_id)
-    replied_accounts += statuses.filter { |status| status.limited_visibility? && status.thread.present? }.map { |status| status.thread.account_id }
-
-    crutches[:following]            = Follow.where(account_id: receiver_id, target_account_id: replied_accounts).pluck(:target_account_id).index_with(true)
+    crutches[:following]            = crutches_following(receiver_id, statuses, list)
     crutches[:languages]            = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
     crutches[:hiding_reblogs]       = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
     crutches[:blocking]             = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
     crutches[:muting]               = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
     crutches[:domain_blocking]      = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)
     crutches[:blocked_by]           = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true)
-    crutches[:exclusive_list_users] = ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
-    crutches[:exclusive_antenna_users] = AntennaAccount.where(antenna: antennas, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
+    crutches[:exclusive_list_users] = crutches_exclusive_list_users(receiver_id, statuses) if list.nil? || stl_home
+    crutches[:exclusive_antenna_users] = crutches_exclusive_antenna_users(receiver_id, statuses)
 
     crutches
   end
 
+  def crutches_exclusive_list_users(recipient_id, statuses)
+    lists = List.where(account_id: recipient_id, exclusive: true)
+    ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
+  end
+
+  def crutches_exclusive_antenna_users(recipient_id, statuses)
+    lists = List.where(account_id: recipient_id, exclusive: true)
+    antennas = Antenna.where(list: lists, insert_feeds: true)
+    AntennaAccount.where(antenna: antennas, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
+  end
+
+  def crutches_following(recipient_id, statuses, list)
+    if list.blank? || list.show_followed?
+      replied_accounts  = statuses.filter_map(&:in_reply_to_account_id)
+      replied_accounts += statuses.filter { |status| status.limited_visibility? && status.thread.present? }.map { |status| status.thread.account_id }
+
+      Follow.where(account_id: recipient_id, target_account_id: replied_accounts).pluck(:target_account_id).index_with(true)
+    elsif list.show_list?
+      ListAccount.where(list_id: list.id, account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:account_id).index_with(true)
+    else
+      {}
+    end
+  end
+
   def crutches_active_mentions(statuses)
     Mention
       .active
diff --git a/app/lib/hashtag_normalizer.rb b/app/lib/hashtag_normalizer.rb
index 49fa6101de..5347271194 100644
--- a/app/lib/hashtag_normalizer.rb
+++ b/app/lib/hashtag_normalizer.rb
@@ -16,7 +16,7 @@ class HashtagNormalizer
   end
 
   def lowercase(str)
-    str.mb_chars.downcase.to_s
+    str.downcase.to_s
   end
 
   def cjk_width(str)
diff --git a/app/lib/plain_text_formatter.rb b/app/lib/plain_text_formatter.rb
index f960ba7acc..e8ff79806f 100644
--- a/app/lib/plain_text_formatter.rb
+++ b/app/lib/plain_text_formatter.rb
@@ -16,7 +16,15 @@ class PlainTextFormatter
     if local?
       text
     else
-      node = Nokogiri::HTML5.fragment(insert_newlines)
+      begin
+        node = Nokogiri::HTML5.fragment(insert_newlines)
+      rescue ArgumentError
+        # This can happen if one of the Nokogumbo limits is encountered
+        # Unfortunately, it does not use a more precise error class
+        # nor allows more graceful handling
+        return ''
+      end
+
       # Elements that are entirely removed with our Sanitize config
       node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
       node.text.chomp
diff --git a/app/lib/request.rb b/app/lib/request.rb
index 4e0ba77833..ad39f928db 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -260,7 +260,7 @@ class Request
         outer_e = nil
         port    = args.first
 
-        addresses = [] # rubocop:disable Lint/UselessAssignment # TODO: https://github.com/rubocop/rubocop/issues/13395
+        addresses = []
         begin
           addresses = [IPAddr.new(host)]
         rescue IPAddr::InvalidAddressError
diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb
index deead3717d..56ffd2a28c 100644
--- a/app/lib/status_cache_hydrator.rb
+++ b/app/lib/status_cache_hydrator.rb
@@ -28,13 +28,7 @@ class StatusCacheHydrator
 
   def hydrate_non_reblog_payload(empty_payload, account_id, account)
     empty_payload.tap do |payload|
-      payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.id)
-      payload[:reblogged]  = Status.exists?(account_id: account_id, reblog_of_id: @status.id)
-      payload[:muted]      = ConversationMute.exists?(account_id: account_id, conversation_id: @status.conversation_id)
-      payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.id)
-      payload[:pinned]     = StatusPin.exists?(account_id: account_id, status_id: @status.id) if @status.account_id == account_id
-      payload[:filtered]   = mapped_applied_custom_filter(account_id, @status)
-      payload[:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
+      fill_status_payload(payload, @status, account_id, account)
 
       if payload[:poll]
         payload[:poll][:voted] = @status.account_id == account_id
@@ -48,19 +42,12 @@ class StatusCacheHydrator
       payload[:muted]      = false
       payload[:bookmarked] = false
       payload[:pinned]     = false if @status.account_id == account_id
-      payload[:filtered]   = mapped_applied_custom_filter(account_id, @status.reblog)
 
       # If the reblogged status is being delivered to the author who disabled the display of the application
       # used to create the status, we need to hydrate it here too
       payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
 
-      payload[:reblog][:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.reblog_of_id)
-      payload[:reblog][:reblogged]  = Status.exists?(account_id: account_id, reblog_of_id: @status.reblog_of_id)
-      payload[:reblog][:muted]      = ConversationMute.exists?(account_id: account_id, conversation_id: @status.reblog.conversation_id)
-      payload[:reblog][:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.reblog_of_id)
-      payload[:reblog][:pinned]     = StatusPin.exists?(account_id: account_id, status_id: @status.reblog_of_id) if @status.reblog.account_id == account_id
-      payload[:reblog][:filtered]   = payload[:filtered]
-      payload[:reblog][:emoji_reactions] = @status.reblog.emoji_reactions_grouped_by_name(account)
+      fill_status_payload(payload[:reblog], @status.reblog, account_id, account)
 
       if payload[:reblog][:poll]
         if @status.reblog.account_id == account_id
@@ -73,11 +60,22 @@ class StatusCacheHydrator
         end
       end
 
+      payload[:filtered]   = payload[:reblog][:filtered]
       payload[:favourited] = payload[:reblog][:favourited]
       payload[:reblogged]  = payload[:reblog][:reblogged]
     end
   end
 
+  def fill_status_payload(payload, status, account_id, account)
+    payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: status.id)
+    payload[:reblogged]  = Status.exists?(account_id: account_id, reblog_of_id: status.id)
+    payload[:muted]      = ConversationMute.exists?(account_id: account_id, conversation_id: status.conversation_id)
+    payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id)
+    payload[:pinned]     = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
+    payload[:filtered]   = mapped_applied_custom_filter(account_id, status)
+    payload[:emoji_reactions] = status.emoji_reactions_grouped_by_name(account)
+  end
+
   def mapped_applied_custom_filter(account_id, status)
     CustomFilter
       .apply_cached_filters(CustomFilter.cached_filters_for(account_id), status, following: following?(account_id))
diff --git a/app/lib/video_metadata_extractor.rb b/app/lib/video_metadata_extractor.rb
index 6e544486d6..33a6264b4f 100644
--- a/app/lib/video_metadata_extractor.rb
+++ b/app/lib/video_metadata_extractor.rb
@@ -12,7 +12,7 @@ class VideoMetadataExtractor
   rescue Terrapin::ExitStatusError, Oj::ParseError
     @invalid = true
   rescue Terrapin::CommandNotFoundError
-    raise Paperclip::Errors::CommandNotFoundError, 'Could not run the `ffprobe` command. Please install ffmpeg.'
+    raise Paperclip::Errors::CommandNotFoundError, 'Could not run the `ffprobe` command. Please install ffmpeg.' # rubocop:disable I18n/RailsI18n/DecorateString -- This error is not user-facing
   end
 
   def valid?
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index b02c462217..0eef9b90da 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -219,6 +219,15 @@ class UserMailer < Devise::Mailer
     end
   end
 
+  def announcement_published(user, announcement)
+    @resource = user
+    @announcement = announcement
+
+    I18n.with_locale(locale) do
+      mail subject: default_i18n_subject
+    end
+  end
+
   private
 
   def default_devise_subject
diff --git a/app/models/account.rb b/app/models/account.rb
index 0612e63fd5..f3f591d006 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -160,7 +160,7 @@ class Account < ApplicationRecord
   scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
   scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
   scope :dormant, -> { joins(:account_stat).merge(AccountStat.without_recent_activity) }
-  scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) }
+  scope :with_username, ->(value) { value.is_a?(Array) ? where(arel_table[:username].lower.in(value.map { |x| x.to_s.downcase })) : where(arel_table[:username].lower.eq(value.to_s.downcase)) }
   scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
   scope :without_memorial, -> { where(memorial: false) }
   scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) }
diff --git a/app/models/account/field.rb b/app/models/account/field.rb
index bcd89015de..4b3ccea9c4 100644
--- a/app/models/account/field.rb
+++ b/app/models/account/field.rb
@@ -73,7 +73,14 @@ class Account::Field < ActiveModelSerializers::Model
   end
 
   def extract_url_from_html
-    doc = Nokogiri::HTML5.fragment(value)
+    begin
+      doc = Nokogiri::HTML5.fragment(value)
+    rescue ArgumentError
+      # This can happen if one of the Nokogumbo limits is encountered
+      # Unfortunately, it does not use a more precise error class
+      # nor allows more graceful handling
+      return
+    end
 
     return if doc.nil?
     return if doc.children.size != 1
diff --git a/app/models/announcement.rb b/app/models/announcement.rb
index 6b3b8ccfc1..ff9c188398 100644
--- a/app/models/announcement.rb
+++ b/app/models/announcement.rb
@@ -4,17 +4,18 @@
 #
 # Table name: announcements
 #
-#  id           :bigint(8)        not null, primary key
-#  text         :text             default(""), not null
-#  published    :boolean          default(FALSE), not null
-#  all_day      :boolean          default(FALSE), not null
-#  scheduled_at :datetime
-#  starts_at    :datetime
-#  ends_at      :datetime
-#  created_at   :datetime         not null
-#  updated_at   :datetime         not null
-#  published_at :datetime
-#  status_ids   :bigint(8)        is an Array
+#  id                   :bigint(8)        not null, primary key
+#  all_day              :boolean          default(FALSE), not null
+#  ends_at              :datetime
+#  notification_sent_at :datetime
+#  published            :boolean          default(FALSE), not null
+#  published_at         :datetime
+#  scheduled_at         :datetime
+#  starts_at            :datetime
+#  status_ids           :bigint(8)        is an Array
+#  text                 :text             default(""), not null
+#  created_at           :datetime         not null
+#  updated_at           :datetime         not null
 #
 
 class Announcement < ApplicationRecord
@@ -54,6 +55,10 @@ class Announcement < ApplicationRecord
     update!(published: false, scheduled_at: nil)
   end
 
+  def notification_sent?
+    notification_sent_at.present?
+  end
+
   def mentions
     @mentions ||= Account.from_text(text)
   end
@@ -86,6 +91,10 @@ class Announcement < ApplicationRecord
     end
   end
 
+  def scope_for_notification
+    User.confirmed.joins(:account).merge(Account.without_suspended)
+  end
+
   private
 
   def grouped_ordered_announcement_reactions
diff --git a/app/models/concerns/account/interactions.rb b/app/models/concerns/account/interactions.rb
index 026f0e6688..259b51c194 100644
--- a/app/models/concerns/account/interactions.rb
+++ b/app/models/concerns/account/interactions.rb
@@ -115,7 +115,7 @@ module Account::Interactions
   end
 
   def follow!(other_account, reblogs: nil, notify: nil, languages: nil, uri: nil, rate_limit: false, bypass_limit: false)
-    rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, languages: languages, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
+    rel = active_relationships.create_with(show_reblogs: reblogs.nil? || reblogs, notify: notify.nil? ? false : notify, languages: languages, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
                               .find_or_create_by!(target_account: other_account)
 
     rel.show_reblogs = reblogs   unless reblogs.nil?
@@ -128,7 +128,7 @@ module Account::Interactions
   end
 
   def request_follow!(other_account, reblogs: nil, notify: nil, languages: nil, uri: nil, rate_limit: false, bypass_limit: false)
-    rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, languages: languages, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
+    rel = follow_requests.create_with(show_reblogs: reblogs.nil? || reblogs, notify: notify.nil? ? false : notify, uri: uri, languages: languages, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
                          .find_or_create_by!(target_account: other_account)
 
     rel.show_reblogs = reblogs   unless reblogs.nil?
diff --git a/app/models/concerns/fasp/provider/debug_concern.rb b/app/models/concerns/fasp/provider/debug_concern.rb
new file mode 100644
index 0000000000..eee046a17f
--- /dev/null
+++ b/app/models/concerns/fasp/provider/debug_concern.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Fasp::Provider::DebugConcern
+  extend ActiveSupport::Concern
+
+  def perform_debug_call
+    Fasp::Request.new(self)
+                 .post('/debug/v0/callback/logs', body: { hello: 'world' })
+  end
+end
diff --git a/app/models/concerns/notification/groups.rb b/app/models/concerns/notification/groups.rb
index e064df8502..bea0dbca05 100644
--- a/app/models/concerns/notification/groups.rb
+++ b/app/models/concerns/notification/groups.rb
@@ -4,16 +4,20 @@ module Notification::Groups
   extend ActiveSupport::Concern
 
   # `set_group_key!` needs to be updated if this list changes
-  GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog follow emoji_reaction).freeze
+  GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog follow emoji_reaction admin.sign_up).freeze
   MAXIMUM_GROUP_SPAN_HOURS = 12
 
+  included do
+    scope :by_group_key, ->(group_key) { group_key&.start_with?('ungrouped-') ? where(id: group_key.delete_prefix('ungrouped-')) : where(group_key: group_key) }
+  end
+
   def set_group_key!
     return if filtered? || GROUPABLE_NOTIFICATION_TYPES.exclude?(type)
 
     type_prefix = case type
                   when :favourite, :reblog, :emoji_reaction
                     [type, target_status&.id].join('-')
-                  when :follow
+                  when :follow, :'admin.sign_up'
                     type
                   else
                     raise NotImplementedError
diff --git a/app/models/concerns/status/fetch_replies_concern.rb b/app/models/concerns/status/fetch_replies_concern.rb
new file mode 100644
index 0000000000..fd9929aba4
--- /dev/null
+++ b/app/models/concerns/status/fetch_replies_concern.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Status::FetchRepliesConcern
+  extend ActiveSupport::Concern
+
+  # enable/disable fetching all replies
+  FETCH_REPLIES_ENABLED = ENV['FETCH_REPLIES_ENABLED'] == 'true'
+
+  # debounce fetching all replies to minimize DoS
+  FETCH_REPLIES_COOLDOWN_MINUTES = (ENV['FETCH_REPLIES_COOLDOWN_MINUTES'] || 15).to_i.minutes
+  FETCH_REPLIES_INITIAL_WAIT_MINUTES = (ENV['FETCH_REPLIES_INITIAL_WAIT_MINUTES'] || 5).to_i.minutes
+
+  included do
+    scope :created_recently, -> { where(created_at: FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago..) }
+    scope :not_created_recently, -> { where(created_at: ..FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago) }
+    scope :fetched_recently, -> { where(fetched_replies_at: FETCH_REPLIES_COOLDOWN_MINUTES.ago..) }
+    scope :not_fetched_recently, -> { where(fetched_replies_at: [nil, ..FETCH_REPLIES_COOLDOWN_MINUTES.ago]) }
+
+    scope :should_not_fetch_replies, -> { local.or(created_recently.or(fetched_recently)) }
+    scope :should_fetch_replies, -> { remote.not_created_recently.not_fetched_recently }
+
+    # statuses for which we won't receive update or deletion actions,
+    # and should update when fetching replies
+    # Status from an account which either
+    # a) has only remote followers
+    # b) has local follows that were created after the last update time, or
+    # c) has no known followers
+    scope :unsubscribed, lambda {
+      remote.merge(
+        Status.left_outer_joins(account: :followers).where.not(followers_accounts: { domain: nil })
+              .or(where.not('follows.created_at < statuses.updated_at'))
+              .or(where(follows: { id: nil }))
+      )
+    }
+  end
+
+  def should_fetch_replies?
+    # we aren't brand new, and we haven't fetched replies since the debounce window
+    FETCH_REPLIES_ENABLED && !local? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && (
+      fetched_replies_at.nil? || fetched_replies_at <= FETCH_REPLIES_COOLDOWN_MINUTES.ago
+    )
+  end
+end
diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb
index 9e0784be28..b67105aade 100644
--- a/app/models/custom_filter.rb
+++ b/app/models/custom_filter.rb
@@ -38,7 +38,7 @@ class CustomFilter < ApplicationRecord
   include Expireable
   include Redisable
 
-  enum :action, { warn: 0, hide: 1 }, suffix: :action
+  enum :action, { warn: 0, hide: 1, blur: 2 }, suffix: :action, validate: true
 
   belongs_to :account
   has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
diff --git a/app/models/fasp.rb b/app/models/fasp.rb
new file mode 100644
index 0000000000..cb33937715
--- /dev/null
+++ b/app/models/fasp.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Fasp
+  def self.table_name_prefix
+    'fasp_'
+  end
+end
diff --git a/app/models/fasp/capability.rb b/app/models/fasp/capability.rb
new file mode 100644
index 0000000000..eb41571e57
--- /dev/null
+++ b/app/models/fasp/capability.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Fasp::Capability
+  include ActiveModel::Model
+  include ActiveModel::Attributes
+
+  attribute :id, :string
+  attribute :version, :string
+  attribute :enabled, :boolean, default: false
+end
diff --git a/app/models/fasp/debug_callback.rb b/app/models/fasp/debug_callback.rb
new file mode 100644
index 0000000000..30f5d1c37d
--- /dev/null
+++ b/app/models/fasp/debug_callback.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: fasp_debug_callbacks
+#
+#  id               :bigint(8)        not null, primary key
+#  ip               :string           not null
+#  request_body     :text             not null
+#  created_at       :datetime         not null
+#  updated_at       :datetime         not null
+#  fasp_provider_id :bigint(8)        not null
+#
+class Fasp::DebugCallback < ApplicationRecord
+  belongs_to :fasp_provider, class_name: 'Fasp::Provider'
+end
diff --git a/app/models/fasp/provider.rb b/app/models/fasp/provider.rb
new file mode 100644
index 0000000000..cd1b3008c7
--- /dev/null
+++ b/app/models/fasp/provider.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: fasp_providers
+#
+#  id                      :bigint(8)        not null, primary key
+#  base_url                :string           not null
+#  capabilities            :jsonb            not null
+#  confirmed               :boolean          default(FALSE), not null
+#  contact_email           :string
+#  fediverse_account       :string
+#  name                    :string           not null
+#  privacy_policy          :jsonb
+#  provider_public_key_pem :string           not null
+#  remote_identifier       :string           not null
+#  server_private_key_pem  :string           not null
+#  sign_in_url             :string
+#  created_at              :datetime         not null
+#  updated_at              :datetime         not null
+#
+class Fasp::Provider < ApplicationRecord
+  include DebugConcern
+
+  has_many :fasp_debug_callbacks, inverse_of: :fasp_provider, class_name: 'Fasp::DebugCallback', dependent: :delete_all
+
+  validates :name, presence: true
+  validates :base_url, presence: true, url: true
+  validates :provider_public_key_pem, presence: true
+  validates :remote_identifier, presence: true
+
+  before_create :create_keypair
+  after_commit :update_remote_capabilities
+
+  def capabilities
+    read_attribute(:capabilities).map do |attributes|
+      Fasp::Capability.new(attributes)
+    end
+  end
+
+  def capabilities_attributes=(attributes)
+    capability_objects = attributes.values.map { |a| Fasp::Capability.new(a) }
+    self[:capabilities] = capability_objects.map(&:attributes)
+  end
+
+  def enabled_capabilities
+    capabilities.select(&:enabled).map(&:id)
+  end
+
+  def capability?(capability_name)
+    return false unless confirmed?
+
+    capabilities.present? && capabilities.any? do |capability|
+      capability.id == capability_name
+    end
+  end
+
+  def capability_enabled?(capability_name)
+    return false unless confirmed?
+
+    capabilities.present? && capabilities.any? do |capability|
+      capability.id == capability_name && capability.enabled
+    end
+  end
+
+  def server_private_key
+    @server_private_key ||= OpenSSL::PKey.read(server_private_key_pem)
+  end
+
+  def server_public_key_base64
+    Base64.strict_encode64(server_private_key.raw_public_key)
+  end
+
+  def provider_public_key_base64=(string)
+    return if string.blank?
+
+    self.provider_public_key_pem =
+      OpenSSL::PKey.new_raw_public_key(
+        'ed25519',
+        Base64.strict_decode64(string)
+      ).public_to_pem
+  end
+
+  def provider_public_key
+    @provider_public_key ||= OpenSSL::PKey.read(provider_public_key_pem)
+  end
+
+  def provider_public_key_raw
+    provider_public_key.raw_public_key
+  end
+
+  def provider_public_key_fingerprint
+    OpenSSL::Digest.base64digest('sha256', provider_public_key_raw)
+  end
+
+  def url(path)
+    base = base_url
+    base = base.chomp('/') if path.start_with?('/')
+    "#{base}#{path}"
+  end
+
+  def update_info!(confirm: false)
+    self.confirmed = true if confirm
+    provider_info = Fasp::Request.new(self).get('/provider_info')
+    assign_attributes(
+      privacy_policy: provider_info['privacyPolicy'],
+      capabilities: provider_info['capabilities'],
+      sign_in_url: provider_info['signInUrl'],
+      contact_email: provider_info['contactEmail'],
+      fediverse_account: provider_info['fediverseAccount']
+    )
+    save!
+  end
+
+  private
+
+  def create_keypair
+    self.server_private_key_pem ||=
+      OpenSSL::PKey.generate_key('ed25519').private_to_pem
+  end
+
+  def update_remote_capabilities
+    return unless saved_change_to_attribute?(:capabilities)
+
+    old, current = saved_change_to_attribute(:capabilities)
+    old ||= []
+    current.each do |capability|
+      update_remote_capability(capability) if capability.key?('enabled') && !old.include?(capability)
+    end
+  end
+
+  def update_remote_capability(capability)
+    version, = capability['version'].split('.')
+    path = "/capabilities/#{capability['id']}/#{version}/activation"
+    if capability['enabled']
+      Fasp::Request.new(self).post(path)
+    else
+      Fasp::Request.new(self).delete(path)
+    end
+  end
+end
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index f9c5b80262..31ac04b754 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -65,6 +65,7 @@ class Form::AdminSettings
     stop_link_preview_domains
     app_icon
     favicon
+    min_age
   ).freeze
 
   INTEGER_KEYS = %i(
@@ -80,6 +81,7 @@ class Form::AdminSettings
     registrations_end_hour
     registrations_secondary_start_hour
     registrations_secondary_end_hour
+    min_age
   ).freeze
 
   BOOLEAN_KEYS = %i(
@@ -140,6 +142,7 @@ class Form::AdminSettings
   validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) }
   validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) }
   validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
+  validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }
   validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) }
   validates :status_page_url, url: true, allow_blank: true
   validate :validate_site_uploads
diff --git a/app/models/import.rb b/app/models/import.rb
deleted file mode 100644
index 6b261f8d00..0000000000
--- a/app/models/import.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-# == Schema Information
-#
-# Table name: imports
-#
-#  id                :bigint(8)        not null, primary key
-#  type              :integer          not null
-#  approved          :boolean          default(FALSE), not null
-#  created_at        :datetime         not null
-#  updated_at        :datetime         not null
-#  data_file_name    :string
-#  data_content_type :string
-#  data_updated_at   :datetime
-#  account_id        :bigint(8)        not null
-#  overwrite         :boolean          default(FALSE), not null
-#  data_file_size    :integer
-#
-
-# NOTE: This is a deprecated model, only kept to not break ongoing imports
-# on upgrade. See `BulkImport` and `Form::Import` for its replacements.
-
-class Import < ApplicationRecord
-  FILE_TYPES = %w(text/plain text/csv application/csv).freeze
-  MODES = %i(merge overwrite).freeze
-
-  self.inheritance_column = false
-
-  belongs_to :account
-
-  enum :type, { following: 0, blocking: 1, muting: 2, domain_blocking: 3, bookmarks: 4 }
-
-  validates :type, presence: true
-
-  has_attached_file :data
-  validates_attachment_content_type :data, content_type: FILE_TYPES
-  validates_attachment_presence :data
-
-  def mode
-    overwrite? ? :overwrite : :merge
-  end
-
-  def mode=(str)
-    self.overwrite = str.to_sym == :overwrite
-  end
-end
diff --git a/app/models/list.rb b/app/models/list.rb
index a568b11776..a441d065cf 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -20,7 +20,7 @@ class List < ApplicationRecord
 
   PER_ACCOUNT_LIMIT = 50
 
-  enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show
+  enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show, validate: true
 
   belongs_to :account
 
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 49ff740884..89c74d5a41 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -116,7 +116,7 @@ class MediaAttachment < ApplicationRecord
   VIDEO_PASSTHROUGH_OPTIONS = {
     video_codecs: ['h264'].freeze,
     audio_codecs: ['aac', nil].freeze,
-    colorspaces: ['yuv420p'].freeze,
+    colorspaces: ['yuv420p', 'yuvj420p'].freeze,
     options: {
       format: 'mp4',
       convert_options: {
@@ -425,8 +425,10 @@ class MediaAttachment < ApplicationRecord
 
     @paths_to_cache_bust = MediaAttachment.attachment_definitions.keys.flat_map do |attachment_name|
       attachment = public_send(attachment_name)
+      next if attachment.blank?
+
       styles = DEFAULT_STYLES | attachment.styles.keys
-      styles.map { |style| attachment.path(style) }
+      styles.map { |style| attachment.url(style) }
     end.compact
   rescue => e
     # We really don't want any error here preventing media deletion
diff --git a/app/models/poll.rb b/app/models/poll.rb
index 7c59339b7e..25ce9c3be9 100644
--- a/app/models/poll.rb
+++ b/app/models/poll.rb
@@ -23,6 +23,8 @@
 class Poll < ApplicationRecord
   include Expireable
 
+  MAKE_FETCH_HAPPEN = 1.minute
+
   belongs_to :account
   belongs_to :status
 
@@ -113,7 +115,7 @@ class Poll < ApplicationRecord
   end
 
   def time_passed_since_last_fetch?
-    last_fetched_at.nil? || last_fetched_at < 1.minute.ago
+    last_fetched_at.nil? || last_fetched_at < MAKE_FETCH_HAPPEN.ago
   end
 
   def show_totals_now?
diff --git a/app/models/status.rb b/app/models/status.rb
index 59ce718c92..0325802022 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -31,6 +31,7 @@
 #  markdown                     :boolean          default(FALSE)
 #  limited_scope                :integer
 #  quote_of_id                  :bigint(8)
+#  fetched_replies_at           :datetime
 #
 
 require 'ostruct'
@@ -41,6 +42,7 @@ class Status < ApplicationRecord
   include Paginable
   include RateLimitable
   include Status::DomainBlockConcern
+  include Status::FetchRepliesConcern
   include Status::SafeReblogInsert
   include Status::SearchConcern
   include Status::SnapshotConcern
@@ -425,7 +427,7 @@ class Status < ApplicationRecord
           end
 
           emoji_reaction['count'] = emoji_reaction['account_ids'].size
-          public_emoji_reactions << emoji_reaction if (emoji_reaction['count']).positive?
+          public_emoji_reactions << emoji_reaction if emoji_reaction['count'].positive?
         end
 
         public_emoji_reactions
@@ -505,7 +507,7 @@ class Status < ApplicationRecord
     end
 
     def bookmarks_map(status_ids, account_id)
-      Bookmark.select(:status_id).where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
+      Bookmark.select(:status_id).where(status_id: status_ids).where(account_id: account_id).to_h { |f| [f.status_id, true] }
     end
 
     def reblogs_map(status_ids, account_id)
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 09d1a1a15b..2fee6bc1ee 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -161,11 +161,11 @@ class Tag < ApplicationRecord
   private
 
   def validate_name_change
-    errors.add(:name, I18n.t('tags.does_not_match_previous_name')) unless name_was.mb_chars.casecmp(name.mb_chars).zero?
+    errors.add(:name, I18n.t('tags.does_not_match_previous_name')) unless name_was.casecmp(name).zero?
   end
 
   def validate_display_name_change
-    unless HashtagNormalizer.new.normalize(display_name).casecmp(name.mb_chars).zero?
+    unless HashtagNormalizer.new.normalize(display_name).casecmp(name).zero?
       errors.add(:display_name,
                  I18n.t('tags.does_not_match_previous_name'))
     end
diff --git a/app/models/terms_of_service.rb b/app/models/terms_of_service.rb
index 1f0832dc9a..3b69a40a1a 100644
--- a/app/models/terms_of_service.rb
+++ b/app/models/terms_of_service.rb
@@ -6,6 +6,7 @@
 #
 #  id                   :bigint(8)        not null, primary key
 #  changelog            :text             default(""), not null
+#  effective_date       :date
 #  notification_sent_at :datetime
 #  published_at         :datetime
 #  text                 :text             default(""), not null
@@ -13,17 +14,27 @@
 #  updated_at           :datetime         not null
 #
 class TermsOfService < ApplicationRecord
-  scope :published, -> { where.not(published_at: nil).order(published_at: :desc) }
-  scope :live, -> { published.limit(1) }
+  scope :published, -> { where.not(published_at: nil).order(Arel.sql('coalesce(effective_date, published_at) DESC')) }
+  scope :live, -> { published.where('effective_date IS NULL OR effective_date < now()').limit(1) }
   scope :draft, -> { where(published_at: nil).order(id: :desc).limit(1) }
 
   validates :text, presence: true
-  validates :changelog, presence: true, if: -> { published? }
+  validates :changelog, :effective_date, presence: true, if: -> { published? }
+
+  validate :effective_date_cannot_be_in_the_past
 
   def published?
     published_at.present?
   end
 
+  def effective?
+    published? && effective_date&.past?
+  end
+
+  def succeeded_by
+    TermsOfService.published.where(effective_date: (effective_date..)).where.not(id: id).first
+  end
+
   def notification_sent?
     notification_sent_at.present?
   end
@@ -31,4 +42,14 @@ class TermsOfService < ApplicationRecord
   def scope_for_notification
     User.confirmed.joins(:account).merge(Account.without_suspended).where(created_at: (..published_at))
   end
+
+  private
+
+  def effective_date_cannot_be_in_the_past
+    return if effective_date.blank?
+
+    min_date = TermsOfService.live.pick(:effective_date) || Time.zone.today
+
+    errors.add(:effective_date, :too_soon, date: min_date) if effective_date < min_date
+  end
 end
diff --git a/app/models/terms_of_service/generator.rb b/app/models/terms_of_service/generator.rb
index 83c229720e..d1c8d413ef 100644
--- a/app/models/terms_of_service/generator.rb
+++ b/app/models/terms_of_service/generator.rb
@@ -9,10 +9,12 @@ class TermsOfService::Generator
     admin_email
     arbitration_address
     arbitration_website
+    choice_of_law
     dmca_address
     dmca_email
     domain
     jurisdiction
+    min_age
   ).freeze
 
   attr_accessor(*VARIABLES)
diff --git a/app/models/trends/links.rb b/app/models/trends/links.rb
index 35ccf7744c..c24e069b51 100644
--- a/app/models/trends/links.rb
+++ b/app/models/trends/links.rb
@@ -33,7 +33,8 @@ class Trends::Links < Trends::Base
   def register(status, at_time = Time.now.utc)
     original_status = status.proper
 
-    return unless (original_status.public_visibility? && status.public_visibility?) &&
+    return unless original_status.public_visibility? &&
+                  status.public_visibility? &&
                   !(original_status.account.silenced? || status.account.silenced?) &&
                   !(original_status.spoiler_text? || original_status.sensitive?)
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 70e8ed2e87..7858ab906d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -5,41 +5,42 @@
 # Table name: users
 #
 #  id                        :bigint(8)        not null, primary key
-#  email                     :string           default(""), not null
-#  created_at                :datetime         not null
-#  updated_at                :datetime         not null
-#  encrypted_password        :string           default(""), not null
-#  reset_password_token      :string
-#  reset_password_sent_at    :datetime
-#  sign_in_count             :integer          default(0), not null
-#  current_sign_in_at        :datetime
-#  last_sign_in_at           :datetime
+#  age_verified_at           :datetime
+#  approved                  :boolean          default(TRUE), not null
+#  chosen_languages          :string           is an Array
+#  confirmation_sent_at      :datetime
 #  confirmation_token        :string
 #  confirmed_at              :datetime
-#  confirmation_sent_at      :datetime
-#  unconfirmed_email         :string
-#  locale                    :string
+#  consumed_timestep         :integer
+#  current_sign_in_at        :datetime
+#  disabled                  :boolean          default(FALSE), not null
+#  email                     :string           default(""), not null
 #  encrypted_otp_secret      :string
 #  encrypted_otp_secret_iv   :string
 #  encrypted_otp_secret_salt :string
-#  consumed_timestep         :integer
-#  otp_required_for_login    :boolean          default(FALSE), not null
+#  encrypted_password        :string           default(""), not null
 #  last_emailed_at           :datetime
+#  last_sign_in_at           :datetime
+#  locale                    :string
 #  otp_backup_codes          :string           is an Array
-#  account_id                :bigint(8)        not null
-#  disabled                  :boolean          default(FALSE), not null
-#  invite_id                 :bigint(8)
-#  chosen_languages          :string           is an Array
-#  created_by_application_id :bigint(8)
-#  approved                  :boolean          default(TRUE), not null
+#  otp_required_for_login    :boolean          default(FALSE), not null
+#  otp_secret                :string
+#  reset_password_sent_at    :datetime
+#  reset_password_token      :string
+#  settings                  :text
+#  sign_in_count             :integer          default(0), not null
 #  sign_in_token             :string
 #  sign_in_token_sent_at     :datetime
-#  webauthn_id               :string
 #  sign_up_ip                :inet
-#  role_id                   :bigint(8)
-#  settings                  :text
 #  time_zone                 :string
-#  otp_secret                :string
+#  unconfirmed_email         :string
+#  created_at                :datetime         not null
+#  updated_at                :datetime         not null
+#  account_id                :bigint(8)        not null
+#  created_by_application_id :bigint(8)
+#  invite_id                 :bigint(8)
+#  role_id                   :bigint(8)
+#  webauthn_id               :string
 #
 
 class User < ApplicationRecord
@@ -116,6 +117,7 @@ class User < ApplicationRecord
   validates_with RegistrationFormTimeValidator, on: :create
   validates :website, absence: true, on: :create
   validates :confirm_password, absence: true, on: :create
+  validates :date_of_birth, presence: true, date_of_birth: true, on: :create, if: -> { Setting.min_age.present? }
   validate :validate_role_elevation
 
   scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) }
@@ -134,6 +136,7 @@ class User < ApplicationRecord
 
   before_validation :sanitize_role
   before_create :set_approved
+  before_create :set_age_verified_at
   after_commit :send_pending_devise_notifications
   after_create_commit :trigger_webhooks
 
@@ -145,7 +148,7 @@ class User < ApplicationRecord
 
   delegate :can?, to: :role
 
-  attr_reader :invite_code
+  attr_reader :invite_code, :date_of_birth
   attr_writer :external, :bypass_invite_request_check, :current_account
 
   def self.those_who_can(*any_of_privileges)
@@ -162,6 +165,17 @@ class User < ApplicationRecord
     Rails.env.local?
   end
 
+  def date_of_birth=(hash_or_string)
+    @date_of_birth = begin
+      if hash_or_string.is_a?(Hash)
+        day, month, year = hash_or_string.values_at(1, 2, 3)
+        "#{day}.#{month}.#{year}"
+      else
+        hash_or_string
+      end
+    end
+  end
+
   def role
     if role_id.nil?
       UserRole.everyone
@@ -455,6 +469,10 @@ class User < ApplicationRecord
     end
   end
 
+  def set_age_verified_at
+    self.age_verified_at = Time.now.utc if Setting.min_age.present?
+  end
+
   def grant_approval_on_confirmation?
     # Re-check approval on confirmation if the server has switched to open registrations
     open_registrations? && !sign_up_from_ip_requires_approval? && !sign_up_email_requires_approval?
diff --git a/app/policies/admin/fasp/provider_policy.rb b/app/policies/admin/fasp/provider_policy.rb
new file mode 100644
index 0000000000..a8088fd37d
--- /dev/null
+++ b/app/policies/admin/fasp/provider_policy.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Admin::Fasp::ProviderPolicy < ApplicationPolicy
+  def index?
+    role.can?(:manage_federation)
+  end
+
+  def show?
+    role.can?(:manage_federation)
+  end
+
+  def create?
+    role.can?(:manage_federation)
+  end
+
+  def update?
+    role.can?(:manage_federation)
+  end
+
+  def destroy?
+    role.can?(:manage_federation)
+  end
+end
diff --git a/app/policies/announcement_policy.rb b/app/policies/announcement_policy.rb
index b5dc6a18af..907a3b1a86 100644
--- a/app/policies/announcement_policy.rb
+++ b/app/policies/announcement_policy.rb
@@ -16,4 +16,8 @@ class AnnouncementPolicy < ApplicationPolicy
   def destroy?
     role.can?(:manage_announcements)
   end
+
+  def distribute?
+    record.published? && !record.notification_sent? && role.can?(:manage_settings)
+  end
 end
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index e7af76047f..b786baedfa 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -61,6 +61,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer
       urls: {
         streaming: Rails.configuration.x.streaming_api_base_url,
         status: object.status_page_url,
+        about: about_url,
+        privacy_policy: privacy_policy_url,
+        terms_of_service: TermsOfService.live.exists? ? terms_of_service_url : nil,
       },
 
       vapid: {
@@ -128,7 +131,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer
       enabled: registrations_enabled?,
       approval_required: Setting.registrations_mode == 'approved' || (Setting.registrations_mode == 'open' && !registrations_in_time?),
       limit_reached: Setting.registrations_mode != 'none' && reach_registrations_limit?,
+      reason_required: Setting.registrations_mode == 'approved' && Setting.require_invite_text,
       message: registrations_enabled? ? nil : registrations_message,
+      min_age: Setting.min_age.presence,
       url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil),
     }
   end
diff --git a/app/serializers/rest/terms_of_service_serializer.rb b/app/serializers/rest/terms_of_service_serializer.rb
new file mode 100644
index 0000000000..c6a6f60aa5
--- /dev/null
+++ b/app/serializers/rest/terms_of_service_serializer.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class REST::TermsOfServiceSerializer < ActiveModel::Serializer
+  attributes :effective_date, :effective, :content, :succeeded_by
+
+  def effective_date
+    (object.effective_date || object.published_at).iso8601
+  end
+
+  def effective
+    object.effective?
+  end
+
+  def succeeded_by
+    object.succeeded_by&.effective_date&.iso8601
+  end
+
+  def content
+    markdown.render(object.text.gsub(/%{domain}/, Rails.configuration.x.local_domain))
+  end
+
+  private
+
+  def markdown
+    @markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true)
+  end
+end
diff --git a/app/serializers/rest/web_push_subscription_serializer.rb b/app/serializers/rest/web_push_subscription_serializer.rb
index 4cb980bb93..01825a3bb0 100644
--- a/app/serializers/rest/web_push_subscription_serializer.rb
+++ b/app/serializers/rest/web_push_subscription_serializer.rb
@@ -6,7 +6,7 @@ class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer
   delegate :standard, to: :object
 
   def alerts
-    (object.data&.dig('alerts') || {}).each_with_object({}) { |(k, v), h| h[k] = ActiveModel::Type::Boolean.new.cast(v) }
+    (object.data&.dig('alerts') || {}).transform_values { |v| ActiveModel::Type::Boolean.new.cast(v) }
   end
 
   def server_key
diff --git a/app/services/activitypub/fetch_all_replies_service.rb b/app/services/activitypub/fetch_all_replies_service.rb
new file mode 100644
index 0000000000..765e5c8ae8
--- /dev/null
+++ b/app/services/activitypub/fetch_all_replies_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService
+  include JsonLdHelper
+
+  # Limit of replies to fetch per status
+  MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_SINGLE'] || 500).to_i
+
+  def call(status_uri, collection_or_uri, max_pages: 1, request_id: nil)
+    @status_uri = status_uri
+
+    super
+  end
+
+  private
+
+  def filter_replies(items)
+    # Find all statuses that we *shouldn't* update the replies for, and use that as a filter.
+    # We don't assume that we have the statuses before they're created,
+    # hence the negative filter -
+    # "keep all these uris except the ones we already have"
+    # instead of
+    # "keep all these uris that match some conditions on existing Status objects"
+    #
+    # Typically we assume the number of replies we *shouldn't* fetch is smaller than the
+    # replies we *should* fetch, so we also minimize the number of uris we should load here.
+    uris = items.map { |item| value_or_id(item) }
+
+    # Expand collection to get replies in the DB that were
+    # - not included in the collection,
+    # - that we have locally
+    # - but we have no local followers and thus don't get updates/deletes for
+    parent_id = Status.where(uri: @status_uri).pick(:id)
+    unless parent_id.nil?
+      unsubscribed_replies = Status
+                             .where.not(uri: uris)
+                             .where(in_reply_to_id: parent_id)
+                             .unsubscribed
+                             .pluck(:uri)
+      uris.concat(unsubscribed_replies)
+    end
+
+    dont_update = Status.where(uri: uris).should_not_fetch_replies.pluck(:uri)
+
+    # touch all statuses that already exist and that we're about to update
+    Status.where(uri: uris).should_fetch_replies.touch_all(:fetched_replies_at)
+
+    # Reject all statuses that we already have in the db
+    uris = (uris - dont_update).take(MAX_REPLIES)
+
+    Rails.logger.debug { "FetchAllRepliesService - #{@collection_or_uri}: Fetching filtered statuses: #{uris}" }
+    uris
+  end
+end
diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb
index 89c3a1b6c0..25c62f3be6 100644
--- a/app/services/activitypub/fetch_featured_collection_service.rb
+++ b/app/services/activitypub/fetch_featured_collection_service.rb
@@ -33,7 +33,7 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
     return collection_or_uri if collection_or_uri.is_a?(Hash)
     return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
 
-    fetch_resource_without_id_validation(collection_or_uri, local_follower, true)
+    fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary)
   end
 
   def process_items(items)
diff --git a/app/services/activitypub/fetch_featured_tags_collection_service.rb b/app/services/activitypub/fetch_featured_tags_collection_service.rb
index a0b3c6036b..ec2422a075 100644
--- a/app/services/activitypub/fetch_featured_tags_collection_service.rb
+++ b/app/services/activitypub/fetch_featured_tags_collection_service.rb
@@ -45,7 +45,7 @@ class ActivityPub::FetchFeaturedTagsCollectionService < BaseService
     return collection_or_uri if collection_or_uri.is_a?(Hash)
     return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
 
-    fetch_resource_without_id_validation(collection_or_uri, local_follower, true)
+    fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary)
   end
 
   def process_items(items)
diff --git a/app/services/activitypub/fetch_references_service.rb b/app/services/activitypub/fetch_references_service.rb
index 0c71af58fd..8f3fce5f22 100644
--- a/app/services/activitypub/fetch_references_service.rb
+++ b/app/services/activitypub/fetch_references_service.rb
@@ -39,11 +39,11 @@ class ActivityPub::FetchReferencesService < BaseService
     #
     # Therefore, retry with correct signatures if this fails.
     begin
-      fetch_resource_without_id_validation(collection_or_uri, nil, true)
+      fetch_resource_without_id_validation(collection_or_uri, nil)
     rescue Mastodon::UnexpectedResponseError => e
       raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
 
-      fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true })
+      fetch_resource_without_id_validation(collection_or_uri, nil, request_options: { with_query_string: true })
     end
   end
 end
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
index 6f8882378f..7173746f2d 100644
--- a/app/services/activitypub/fetch_remote_status_service.rb
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -13,7 +13,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
 
     @request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
     @json = if prefetched_body.nil?
-              fetch_resource(uri, true, on_behalf_of)
+              fetch_status(uri, true, on_behalf_of)
             else
               body_to_json(prefetched_body, compare_id: uri)
             end
@@ -80,4 +80,20 @@ class ActivityPub::FetchRemoteStatusService < BaseService
   def expected_object_type?
     equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
   end
+
+  def fetch_status(uri, id_is_known, on_behalf_of = nil)
+    begin
+      fetch_resource(uri, id_is_known, on_behalf_of, raise_on_error: :all)
+    rescue Mastodon::UnexpectedResponseError => e
+      return unless e.response.code == 404
+
+      # If this is a 404 from a public status from a remote account, delete it
+      existing_status = Status.remote.find_by(uri: uri)
+      if existing_status&.distributable?
+        Rails.logger.debug { "FetchRemoteStatusService - Got 404 for orphaned status with URI #{uri}, deleting" }
+        Tombstone.find_or_create_by(uri: uri, account: existing_status.account)
+        RemoveStatusService.new.call(existing_status, redraft: false)
+      end
+    end
+  end
 end
diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb
index 46cab6caf9..f2e4f45104 100644
--- a/app/services/activitypub/fetch_replies_service.rb
+++ b/app/services/activitypub/fetch_replies_service.rb
@@ -3,39 +3,59 @@
 class ActivityPub::FetchRepliesService < BaseService
   include JsonLdHelper
 
-  def call(parent_status, collection_or_uri, allow_synchronous_requests: true, request_id: nil)
-    @account = parent_status.account
+  # Limit of fetched replies
+  MAX_REPLIES = 5
+
+  def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, request_id: nil)
+    @reference_uri = reference_uri
     @allow_synchronous_requests = allow_synchronous_requests
 
-    @items = collection_items(collection_or_uri)
+    @items, n_pages = collection_items(collection_or_uri, max_pages: max_pages)
     return if @items.nil?
 
-    FetchReplyWorker.push_bulk(filtered_replies) { |reply_uri| [reply_uri, { 'request_id' => request_id }] }
+    @items = filter_replies(@items)
+    FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id }] }
 
-    @items
+    [@items, n_pages]
   end
 
   private
 
-  def collection_items(collection_or_uri)
+  def collection_items(collection_or_uri, max_pages: 1)
     collection = fetch_collection(collection_or_uri)
     return unless collection.is_a?(Hash)
 
     collection = fetch_collection(collection['first']) if collection['first'].present?
     return unless collection.is_a?(Hash)
 
+    items = []
+    n_pages = 1
+    while collection.is_a?(Hash)
+      items.concat(as_array(collection_page_items(collection)))
+
+      break if items.size >= MAX_REPLIES
+      break if n_pages >= max_pages
+
+      collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
+      n_pages += 1
+    end
+
+    [items, n_pages]
+  end
+
+  def collection_page_items(collection)
     case collection['type']
     when 'Collection', 'CollectionPage'
-      as_array(collection['items'])
+      collection['items']
     when 'OrderedCollection', 'OrderedCollectionPage'
-      as_array(collection['orderedItems'])
+      collection['orderedItems']
     end
   end
 
   def fetch_collection(collection_or_uri)
     return collection_or_uri if collection_or_uri.is_a?(Hash)
     return unless @allow_synchronous_requests
-    return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
+    return if non_matching_uri_hosts?(@reference_uri, collection_or_uri)
 
     # NOTE: For backward compatibility reasons, Mastodon signs outgoing
     # queries incorrectly by default.
@@ -45,19 +65,19 @@ class ActivityPub::FetchRepliesService < BaseService
     #
     # Therefore, retry with correct signatures if this fails.
     begin
-      fetch_resource_without_id_validation(collection_or_uri, nil, true)
+      fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
     rescue Mastodon::UnexpectedResponseError => e
       raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
 
-      fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { omit_query_string: false })
+      fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary, request_options: { omit_query_string: false })
     end
   end
 
-  def filtered_replies
+  def filter_replies(items)
     # Only fetch replies to the same server as the original status to avoid
     # amplification attacks.
 
     # Also limit to 5 fetched replies to limit potential for DoS.
-    @items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@account.uri, uri) }.take(5)
+    items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@reference_uri, uri) }.take(MAX_REPLIES)
   end
 end
diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb
index 1be39aaedf..10224f4d7e 100644
--- a/app/services/activitypub/process_status_update_service.rb
+++ b/app/services/activitypub/process_status_update_service.rb
@@ -296,7 +296,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
       nil
     end
 
-    @status.mentions.upsert_all(currently_mentioned_account_ids.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id))
+    @status.mentions.upsert_all(currently_mentioned_account_ids.uniq.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id))
 
     # If previous mentions are no longer contained in the text, convert them
     # to silent mentions, since withdrawing access from someone who already
diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb
index f51d671a00..fd6fd1b899 100644
--- a/app/services/activitypub/synchronize_followers_service.rb
+++ b/app/services/activitypub/synchronize_followers_service.rb
@@ -4,32 +4,43 @@ class ActivityPub::SynchronizeFollowersService < BaseService
   include JsonLdHelper
   include Payloadable
 
+  MAX_COLLECTION_PAGES = 10
+
   def call(account, partial_collection_url)
     @account = account
+    @expected_followers_ids = []
 
-    items = collection_items(partial_collection_url)
-    return if items.nil?
-
-    # There could be unresolved accounts (hence the call to .compact) but this
-    # should never happen in practice, since in almost all cases we keep an
-    # Account record, and should we not do that, we should have sent a Delete.
-    # In any case there is not much we can do if that occurs.
-    @expected_followers = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) }
+    return unless process_collection!(partial_collection_url)
 
     remove_unexpected_local_followers!
-    handle_unexpected_outgoing_follows!
   end
 
   private
 
+  def process_page!(items)
+    page_expected_followers = extract_local_followers(items)
+    @expected_followers_ids.concat(page_expected_followers.pluck(:id))
+
+    handle_unexpected_outgoing_follows!(page_expected_followers)
+  end
+
+  def extract_local_followers(items)
+    # There could be unresolved accounts (hence the call to .filter_map) but this
+    # should never happen in practice, since in almost all cases we keep an
+    # Account record, and should we not do that, we should have sent a Delete.
+    # In any case there is not much we can do if that occurs.
+
+    ActivityPub::TagManager.instance.uris_to_local_accounts(items)
+  end
+
   def remove_unexpected_local_followers!
-    @account.followers.local.where.not(id: @expected_followers.map(&:id)).reorder(nil).find_each do |unexpected_follower|
+    @account.followers.local.where.not(id: @expected_followers_ids).reorder(nil).find_each do |unexpected_follower|
       UnfollowService.new.call(unexpected_follower, @account)
     end
   end
 
-  def handle_unexpected_outgoing_follows!
-    @expected_followers.each do |expected_follower|
+  def handle_unexpected_outgoing_follows!(expected_followers)
+    expected_followers.each do |expected_follower|
       next if expected_follower.following?(@account)
 
       if expected_follower.requested?(@account)
@@ -50,18 +61,33 @@ class ActivityPub::SynchronizeFollowersService < BaseService
     Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
   end
 
-  def collection_items(collection_or_uri)
-    collection = fetch_collection(collection_or_uri)
-    return unless collection.is_a?(Hash)
+  # Only returns true if the whole collection has been processed
+  def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES)
+    collection = fetch_collection(collection_uri)
+    return false unless collection.is_a?(Hash)
 
     collection = fetch_collection(collection['first']) if collection['first'].present?
-    return unless collection.is_a?(Hash)
 
+    while collection.is_a?(Hash)
+      process_page!(as_array(collection_page_items(collection)))
+
+      max_pages -= 1
+
+      return true if collection['next'].blank? # We reached the end of the collection
+      return false if max_pages <= 0 # We reached our pages limit
+
+      collection = fetch_collection(collection['next'])
+    end
+
+    false
+  end
+
+  def collection_page_items(collection)
     case collection['type']
     when 'Collection', 'CollectionPage'
-      as_array(collection['items'])
+      collection['items']
     when 'OrderedCollection', 'OrderedCollectionPage'
-      as_array(collection['orderedItems'])
+      collection['orderedItems']
     end
   end
 
@@ -69,6 +95,6 @@ class ActivityPub::SynchronizeFollowersService < BaseService
     return collection_or_uri if collection_or_uri.is_a?(Hash)
     return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
 
-    fetch_resource_without_id_validation(collection_or_uri, nil, true)
+    fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
   end
 end
diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb
index 7665880115..a4399efd65 100644
--- a/app/services/app_sign_up_service.rb
+++ b/app/services/app_sign_up_service.rb
@@ -41,7 +41,7 @@ class AppSignUpService < BaseService
   end
 
   def user_params
-    @params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code)
+    @params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code, :date_of_birth)
   end
 
   def account_params
diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb
index 8bc90968b7..0bf5a2ecbb 100644
--- a/app/services/backup_service.rb
+++ b/app/services/backup_service.rb
@@ -6,6 +6,8 @@ class BackupService < BaseService
   include Payloadable
   include ContextHelper
 
+  CHUNK_SIZE = 1.megabyte
+
   attr_reader :account, :backup
 
   def call(backup)
@@ -181,8 +183,6 @@ class BackupService < BaseService
     ).as_json
   end
 
-  CHUNK_SIZE = 1.megabyte
-
   def download_to_zip(zipfile, attachment, filename)
     adapter = Paperclip.io_adapters.for(attachment)
 
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index f5c5cb7109..d03b889c12 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -101,7 +101,7 @@ class BatchedRemoveStatusService < BaseService
       pipeline.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload)
     end
 
-    status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag|
+    status.tags.map { |tag| tag.name.downcase }.each do |hashtag|
       pipeline.publish("timeline:hashtag:#{hashtag}", payload)
       pipeline.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
     end
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index 0c5bfaefeb..1db15d9ccb 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -50,10 +50,14 @@ class BlockDomainService < BaseService
   def notify_of_severed_relationships!
     return if @domain_block_event.nil?
 
-    # TODO: check how efficient that query is, also check `push_bulk`/`perform_bulk`
-    @domain_block_event.affected_local_accounts.reorder(nil).find_each do |account|
-      event = AccountRelationshipSeveranceEvent.create!(account: account, relationship_severance_event: @domain_block_event)
-      LocalNotificationWorker.perform_async(account.id, event.id, 'AccountRelationshipSeveranceEvent', 'severed_relationships')
+    # find_in_batches and perform_bulk both default to batches of 1000
+    @domain_block_event.affected_local_accounts.reorder(nil).find_in_batches do |accounts|
+      notification_jobs_args = accounts.map do |account|
+        event = AccountRelationshipSeveranceEvent.create!(account:, relationship_severance_event: @domain_block_event)
+        [account.id, event.id, 'AccountRelationshipSeveranceEvent', 'severed_relationships']
+      end
+
+      LocalNotificationWorker.perform_bulk(notification_jobs_args)
     end
   end
 
diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb
index 4fa534c6d1..42ed994a7f 100644
--- a/app/services/concerns/payloadable.rb
+++ b/app/services/concerns/payloadable.rb
@@ -19,7 +19,7 @@ module Payloadable
     object      = record.respond_to?(:virtual_object) ? record.virtual_object : record
     bearcap     = object.is_a?(String) && record.respond_to?(:type) && ['Create', 'Update'].include?(record.type)
 
-    if ((object.respond_to?(:sign?) && object.sign?) && signer && (always_sign || signing_enabled?)) || bearcap || (signer && always_sign_unsafe)
+    if (object.respond_to?(:sign?) && object.sign? && signer && (always_sign || signing_enabled?)) || bearcap || (signer && always_sign_unsafe)
       ActivityPub::LinkedDataSignature.new(payload).sign!(signer, sign_with: sign_with)
     else
       payload
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index f61fb60174..590c7c8e82 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -177,8 +177,8 @@ class FanOutOnWriteService < BaseService
 
   def broadcast_to_hashtag_streams!
     @status.tags.map(&:name).each do |hashtag|
-      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", anonymous_payload)
-      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", anonymous_payload) if @status.local? && Setting.enable_local_timeline
+      redis.publish("timeline:hashtag:#{hashtag.downcase}", anonymous_payload)
+      redis.publish("timeline:hashtag:#{hashtag.downcase}:local", anonymous_payload) if @status.local? && Setting.enable_local_timeline
     end
   end
 
diff --git a/app/services/import_service.rb b/app/services/import_service.rb
deleted file mode 100644
index a695df2fc9..0000000000
--- a/app/services/import_service.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-# frozen_string_literal: true
-
-require 'csv'
-
-# NOTE: This is a deprecated service, only kept to not break ongoing imports
-# on upgrade. See `BulkImportService` for its replacement.
-
-class ImportService < BaseService
-  ROWS_PROCESSING_LIMIT = 20_000
-
-  def call(import)
-    @import  = import
-    @account = @import.account
-
-    case @import.type
-    when 'following'
-      import_follows!
-    when 'blocking'
-      import_blocks!
-    when 'muting'
-      import_mutes!
-    when 'domain_blocking'
-      import_domain_blocks!
-    when 'bookmarks'
-      import_bookmarks!
-    end
-  end
-
-  private
-
-  def import_follows!
-    parse_import_data!(['Account address'])
-    import_relationships!('follow', 'unfollow', @account.following, ROWS_PROCESSING_LIMIT, reblogs: { header: 'Show boosts', default: true }, notify: { header: 'Notify on new posts', default: false }, languages: { header: 'Languages', default: nil })
-  end
-
-  def import_blocks!
-    parse_import_data!(['Account address'])
-    import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT)
-  end
-
-  def import_mutes!
-    parse_import_data!(['Account address'])
-    import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: { header: 'Hide notifications', default: true })
-  end
-
-  def import_domain_blocks!
-    parse_import_data!(['#domain'])
-    items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#domain'].strip }
-
-    if @import.overwrite?
-      presence_hash = items.index_with(true)
-
-      @account.domain_blocks.find_each do |domain_block|
-        if presence_hash[domain_block.domain]
-          items.delete(domain_block.domain)
-        else
-          @account.unblock_domain!(domain_block.domain)
-        end
-      end
-    end
-
-    items.each do |domain|
-      @account.block_domain!(domain)
-    end
-
-    AfterAccountDomainBlockWorker.push_bulk(items) do |domain|
-      [@account.id, domain]
-    end
-  end
-
-  def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
-    local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
-    items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), extra_fields.to_h { |key, field_settings| [key, row[field_settings[:header]]&.strip || field_settings[:default]] }] }.reject { |(id, _)| id.blank? }
-
-    if @import.overwrite?
-      presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
-
-      overwrite_scope.reorder(nil).find_each do |target_account|
-        if presence_hash[target_account.acct]
-          items.delete(target_account.acct)
-          extra = presence_hash[target_account.acct][1]
-          Import::RelationshipWorker.perform_async(@account.id, target_account.acct, action, extra.stringify_keys)
-        else
-          Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
-        end
-      end
-    end
-
-    head_items = items.uniq { |acct, _| acct.split('@')[1] }
-    tail_items = items - head_items
-
-    Import::RelationshipWorker.push_bulk(head_items + tail_items) do |acct, extra|
-      [@account.id, acct, action, extra.stringify_keys]
-    end
-  end
-
-  def import_bookmarks!
-    parse_import_data!(['#uri'])
-    items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#uri'].strip }
-
-    if @import.overwrite?
-      presence_hash = items.index_with(true)
-
-      @account.bookmarks.find_each do |bookmark|
-        if presence_hash[bookmark.status.uri]
-          items.delete(bookmark.status.uri)
-        else
-          bookmark.destroy!
-        end
-      end
-    end
-
-    statuses = items.filter_map do |uri|
-      status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
-      next if status.nil? && ActivityPub::TagManager.instance.local_uri?(uri)
-
-      status || ActivityPub::FetchRemoteStatusService.new.call(uri)
-    rescue *Mastodon::HTTP_CONNECTION_ERRORS, Mastodon::UnexpectedResponseError
-      nil
-    rescue => e
-      Rails.logger.warn "Unexpected error when importing bookmark: #{e}"
-      nil
-    end
-
-    account_ids         = statuses.map(&:account_id)
-    preloaded_relations = @account.relations_map(account_ids, skip_blocking_and_muting: true)
-
-    statuses.keep_if { |status| StatusPolicy.new(@account, status, preloaded_relations).show? }
-
-    statuses.each do |status|
-      @account.bookmarks.find_or_create_by!(account: @account, status: status)
-    end
-  end
-
-  def parse_import_data!(default_headers)
-    data = CSV.parse(import_data, headers: true)
-    data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(' ')
-    @data = data.compact_blank
-  end
-
-  def import_data
-    Paperclip.io_adapters.for(@import.data).read.force_encoding(Encoding::UTF_8)
-  end
-end
diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
index 86aad50983..a591c90913 100644
--- a/app/services/precompute_feed_service.rb
+++ b/app/services/precompute_feed_service.rb
@@ -3,13 +3,21 @@
 class PrecomputeFeedService < BaseService
   include Redisable
 
-  def call(account)
-    FeedManager.instance.populate_home(account)
+  def call(account, skip_filled_timelines: false)
+    @skip_filled_timelines = skip_filled_timelines
+
+    FeedManager.instance.populate_home(account) unless skip_timeline?(:home, account.id)
 
     account.owned_lists.each do |list|
-      FeedManager.instance.populate_list(list)
+      FeedManager.instance.populate_list(list) unless skip_timeline?(:list, list.id)
     end
   ensure
     redis.del("account:#{account.id}:regeneration")
   end
+
+  private
+
+  def skip_timeline?(type, id)
+    @skip_filled_timelines && FeedManager.instance.timeline_size(type, id) * 2 > FeedManager::MAX_ITEMS
+  end
 end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index eb07152ff8..5cc28481a7 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -154,8 +154,8 @@ class RemoveStatusService < BaseService
     return if skip_streaming?
 
     @status.tags.map(&:name).each do |hashtag|
-      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
-      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local?
+      redis.publish("timeline:hashtag:#{hashtag.downcase}", @payload)
+      redis.publish("timeline:hashtag:#{hashtag.downcase}:local", @payload) if @status.local?
     end
   end
 
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 44210799f9..3934a738f7 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -95,7 +95,7 @@ class SuspendAccountService < BaseService
             end
           end
 
-          CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled
+          CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled
         end
       end
     end
diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb
index 652dd6a845..7d3bb806a6 100644
--- a/app/services/unsuspend_account_service.rb
+++ b/app/services/unsuspend_account_service.rb
@@ -91,7 +91,7 @@ class UnsuspendAccountService < BaseService
             end
           end
 
-          CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled
+          CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled
         end
       end
     end
diff --git a/app/validators/date_of_birth_validator.rb b/app/validators/date_of_birth_validator.rb
new file mode 100644
index 0000000000..79119d2c4c
--- /dev/null
+++ b/app/validators/date_of_birth_validator.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class DateOfBirthValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    record.errors.add(attribute, :below_limit) if value.present? && value.to_date > min_age.ago
+  rescue Date::Error
+    record.errors.add(attribute, :invalid)
+  end
+
+  private
+
+  def min_age
+    Setting.min_age.to_i.years
+  end
+end
diff --git a/app/validators/note_length_validator.rb b/app/validators/note_length_validator.rb
index 554ad49ce2..1a16bbf2b3 100644
--- a/app/validators/note_length_validator.rb
+++ b/app/validators/note_length_validator.rb
@@ -8,7 +8,7 @@ class NoteLengthValidator < ActiveModel::EachValidator
   private
 
   def too_long?(value)
-    countable_text(value).mb_chars.grapheme_length > options[:maximum]
+    countable_text(value).each_grapheme_cluster.size > options[:maximum]
   end
 
   def countable_text(value)
diff --git a/app/validators/poll_options_validator.rb b/app/validators/poll_options_validator.rb
index 7ce16fe409..387471544c 100644
--- a/app/validators/poll_options_validator.rb
+++ b/app/validators/poll_options_validator.rb
@@ -7,7 +7,7 @@ class PollOptionsValidator < ActiveModel::Validator
   def validate(poll)
     poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1
     poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS
-    poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS }
+    poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.each_grapheme_cluster.size > MAX_OPTION_CHARS }
     poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size
   end
 end
diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb
index dc841ded3e..d77a0ac610 100644
--- a/app/validators/status_length_validator.rb
+++ b/app/validators/status_length_validator.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class StatusLengthValidator < ActiveModel::Validator
-  MAX_CHARS = 500
+  MAX_CHARS = (ENV['MAX_CHARS'] || 500).to_i
   URL_PLACEHOLDER_CHARS = 23
   URL_PLACEHOLDER = 'x' * 23
 
@@ -18,7 +18,7 @@ class StatusLengthValidator < ActiveModel::Validator
   end
 
   def countable_length(str)
-    str.mb_chars.grapheme_length
+    str.each_grapheme_cluster.size
   end
 
   def combined_text(status)
diff --git a/app/views/admin/announcements/_announcement.html.haml b/app/views/admin/announcements/_announcement.html.haml
index 8190f87d2f..87ae97cf48 100644
--- a/app/views/admin/announcements/_announcement.html.haml
+++ b/app/views/admin/announcements/_announcement.html.haml
@@ -10,6 +10,8 @@
         = l(announcement.created_at)
 
     %div
+      - if can?(:distribute, announcement)
+        = table_link_to 'mail', t('admin.terms_of_service.notify_users'), admin_announcement_preview_path(announcement)
       - if can?(:update, announcement)
         - if announcement.published?
           = table_link_to 'toggle_off', t('admin.announcements.unpublish'), unpublish_admin_announcement_path(announcement), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/admin/announcements/previews/show.html.haml b/app/views/admin/announcements/previews/show.html.haml
new file mode 100644
index 0000000000..54d5d45ed6
--- /dev/null
+++ b/app/views/admin/announcements/previews/show.html.haml
@@ -0,0 +1,22 @@
+- content_for :page_title do
+  = t('admin.announcements.preview.title')
+
+- content_for :heading_actions do
+  .back-link
+    = link_to admin_announcements_path do
+      = material_symbol 'chevron_left'
+      = t('admin.announcements.back')
+
+.flash-message.info= t('admin.announcements.preview.disclaimer')
+
+%p.lead
+  = t('admin.announcements.preview.explanation_html', count: @user_count, display_count: number_with_delimiter(@user_count))
+
+.prose
+  = linkify(@announcement.text)
+
+%hr.spacer/
+
+.content__heading__actions
+  = link_to t('admin.terms_of_service.preview.send_preview', email: current_user.email), admin_announcement_test_path(@announcement), method: :post, class: 'button button-secondary'
+  = link_to t('admin.terms_of_service.preview.send_to_all', count: @user_count, display_count: number_with_delimiter(@user_count)), admin_announcement_distribution_path(@announcement), method: :post, class: 'button', data: { confirm: t('admin.reports.are_you_sure') }
diff --git a/app/views/admin/fasp/debug/callbacks/_callback.html.haml b/app/views/admin/fasp/debug/callbacks/_callback.html.haml
new file mode 100644
index 0000000000..6b6d5cfd04
--- /dev/null
+++ b/app/views/admin/fasp/debug/callbacks/_callback.html.haml
@@ -0,0 +1,10 @@
+%tr
+  %td= callback.fasp_provider.name
+  %td= callback.fasp_provider.base_url
+  %td= callback.ip
+  %td
+    %time.relative-formatted{ datetime: callback.created_at.iso8601 }
+  %td
+    %code= callback.request_body
+  %td
+    = table_link_to 'close', t('admin.fasp.debug.callbacks.delete'), admin_fasp_debug_callback_path(callback), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/admin/fasp/debug/callbacks/index.html.haml b/app/views/admin/fasp/debug/callbacks/index.html.haml
new file mode 100644
index 0000000000..d83ae95fa5
--- /dev/null
+++ b/app/views/admin/fasp/debug/callbacks/index.html.haml
@@ -0,0 +1,22 @@
+- content_for :page_title do
+  = t('admin.fasp.debug.callbacks.title')
+
+- content_for :heading do
+  %h2= t('admin.fasp.debug.callbacks.title')
+  = render 'admin/fasp/shared/links'
+
+- unless @callbacks.empty?
+  %hr.spacer
+
+  .table-wrapper
+    %table.table
+      %thead
+        %tr
+          %th= t('admin.fasp.providers.name')
+          %th= t('admin.fasp.providers.base_url')
+          %th= t('admin.fasp.debug.callbacks.ip')
+          %th= t('admin.fasp.debug.callbacks.created_at')
+          %th= t('admin.fasp.debug.callbacks.request_body')
+          %th
+      %tbody
+        = render partial: 'callback', collection: @callbacks
diff --git a/app/views/admin/fasp/providers/_provider.html.haml b/app/views/admin/fasp/providers/_provider.html.haml
new file mode 100644
index 0000000000..6184daac7f
--- /dev/null
+++ b/app/views/admin/fasp/providers/_provider.html.haml
@@ -0,0 +1,19 @@
+%tr
+  %td= provider.name
+  %td= provider.base_url
+  %td
+    - if provider.confirmed?
+      = t('admin.fasp.providers.active')
+    - else
+      = t('admin.fasp.providers.registration_requested')
+  %td
+    - if provider.confirmed?
+      = table_link_to 'edit', t('admin.fasp.providers.edit'), edit_admin_fasp_provider_path(provider)
+    - else
+      = table_link_to 'check', t('admin.fasp.providers.finish_registration'), new_admin_fasp_provider_registration_path(provider)
+    - if provider.sign_in_url.present?
+      = table_link_to 'open_in_new', t('admin.fasp.providers.sign_in'), provider.sign_in_url, target: '_blank'
+    - if provider.capability_enabled?('callback')
+      = table_link_to 'repeat', t('admin.fasp.providers.callback'), admin_fasp_provider_debug_calls_path(provider), data: { method: :post }
+
+    = table_link_to 'close', t('admin.fasp.providers.delete'), admin_fasp_provider_path(provider), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/admin/fasp/providers/edit.html.haml b/app/views/admin/fasp/providers/edit.html.haml
new file mode 100644
index 0000000000..f4a799c777
--- /dev/null
+++ b/app/views/admin/fasp/providers/edit.html.haml
@@ -0,0 +1,16 @@
+- content_for :page_title do
+  = t('admin.fasp.providers.edit')
+
+= simple_form_for [:admin, @provider] do |f|
+  = render 'shared/error_messages', object: @provider
+
+  %h4= t('admin.fasp.providers.select_capabilities')
+
+  .fields_group
+    = f.fields_for :capabilities do |cf|
+      = cf.input :id, as: :hidden
+      = cf.input :version, as: :hidden
+      = cf.input :enabled, as: :boolean, label: cf.object.id, wrapper: :with_label
+
+  .actions
+    = f.button :button, t('admin.fasp.providers.save'), type: :submit
diff --git a/app/views/admin/fasp/providers/index.html.haml b/app/views/admin/fasp/providers/index.html.haml
new file mode 100644
index 0000000000..209f7e8034
--- /dev/null
+++ b/app/views/admin/fasp/providers/index.html.haml
@@ -0,0 +1,20 @@
+- content_for :page_title do
+  = t('admin.fasp.providers.title')
+
+- content_for :heading do
+  %h2= t('admin.fasp.providers.title')
+  = render 'admin/fasp/shared/links'
+
+- unless @providers.empty?
+  %hr.spacer
+
+  .table-wrapper
+    %table.table#providers
+      %thead
+        %tr
+          %th= t('admin.fasp.providers.name')
+          %th= t('admin.fasp.providers.base_url')
+          %th= t('admin.fasp.providers.status')
+          %th
+      %tbody
+        = render partial: 'provider', collection: @providers
diff --git a/app/views/admin/fasp/registrations/new.html.haml b/app/views/admin/fasp/registrations/new.html.haml
new file mode 100644
index 0000000000..68eb940c09
--- /dev/null
+++ b/app/views/admin/fasp/registrations/new.html.haml
@@ -0,0 +1,19 @@
+- content_for :page_title do
+  = t('admin.fasp.providers.registrations.title')
+
+%p= t('admin.fasp.providers.registrations.description')
+
+%table.table.inline-table
+  %tbody
+    %tr
+      %th= t('admin.fasp.providers.name')
+      %td= @provider.name
+    %tr
+      %th= t('admin.fasp.providers.public_key_fingerprint')
+      %td
+        %code= @provider.provider_public_key_fingerprint
+
+= form_with url: admin_fasp_provider_registration_path(@provider), class: :simple_form do |f|
+  .actions
+    = link_to t('admin.fasp.providers.registrations.reject'), admin_fasp_provider_path(@provider), data: { method: :delete }, class: 'btn negative'
+    = f.button t('admin.fasp.providers.registrations.confirm'), type: :submit, class: 'btn'
diff --git a/app/views/admin/fasp/shared/_links.html.haml b/app/views/admin/fasp/shared/_links.html.haml
new file mode 100644
index 0000000000..0c1d1eb4db
--- /dev/null
+++ b/app/views/admin/fasp/shared/_links.html.haml
@@ -0,0 +1,5 @@
+.content__heading__tabs
+  = render_navigation renderer: :links do |primary|
+    :ruby
+      primary.item :providers, safe_join([material_symbol('database'), t('admin.fasp.providers.providers')]), admin_fasp_providers_path
+      primary.item :debug_callbacks, safe_join([material_symbol('repeat'), t('admin.fasp.debug.callbacks.title')]), admin_fasp_debug_callbacks_path
diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml
index fe6a71a4ca..4bd384f8b5 100644
--- a/app/views/admin/settings/registrations/show.html.haml
+++ b/app/views/admin/settings/registrations/show.html.haml
@@ -12,6 +12,9 @@
 
   .flash-message= t('admin.settings.registrations.moderation_recommandation')
 
+  .fields-group
+    = f.input :min_age, as: :string, wrapper: :with_block_label, input_html: { inputmode: 'numeric' }
+
   .fields-row
     .fields-row__column.fields-row__column-6.fields-group
       = f.input :registrations_mode,
diff --git a/app/views/admin/terms_of_service/drafts/show.html.haml b/app/views/admin/terms_of_service/drafts/show.html.haml
index 7a9a6fd3c4..e83bb47c6b 100644
--- a/app/views/admin/terms_of_service/drafts/show.html.haml
+++ b/app/views/admin/terms_of_service/drafts/show.html.haml
@@ -14,6 +14,9 @@
   .fields-group
     = form.input :changelog, wrapper: :with_block_label, input_html: { rows: 8 }
 
+  .fields-group
+    = form.input :effective_date, wrapper: :with_block_label, as: :date, start_year: Time.zone.today.year
+
   .actions
     = form.button :button, t('admin.terms_of_service.save_draft'), type: :submit, name: :action_type, value: :save_draft, class: 'button button-secondary'
     = form.button :button, t('admin.terms_of_service.publish'), type: :submit, name: :action_type, value: :publish
diff --git a/app/views/admin/terms_of_service/generates/show.html.haml b/app/views/admin/terms_of_service/generates/show.html.haml
index 46737e8355..9fd06a65da 100644
--- a/app/views/admin/terms_of_service/generates/show.html.haml
+++ b/app/views/admin/terms_of_service/generates/show.html.haml
@@ -19,9 +19,15 @@
   .fields-group
     = form.input :domain, wrapper: :with_label
 
+  .fields-group
+    = form.input :min_age, wrapper: :with_label
+
   .fields-group
     = form.input :jurisdiction, wrapper: :with_label
 
+  .fields-group
+    = form.input :choice_of_law, wrapper: :with_label
+
   .fields-group
     = form.input :admin_email, wrapper: :with_label
 
diff --git a/app/views/admin/terms_of_service/histories/show.html.haml b/app/views/admin/terms_of_service/histories/show.html.haml
index 8b7b8eb82e..10921d922f 100644
--- a/app/views/admin/terms_of_service/histories/show.html.haml
+++ b/app/views/admin/terms_of_service/histories/show.html.haml
@@ -12,5 +12,9 @@
     - @terms_of_service.each do |terms_of_service|
       %li
         .admin__terms-of-service__history__item
-          %h5= l(terms_of_service.published_at)
+          %h5
+            - if terms_of_service.effective_date.present?
+              = link_to l(terms_of_service.published_at), terms_of_service_version_path(date: terms_of_service.effective_date)
+            - else
+              = l(terms_of_service.published_at)
           .prose= markdown(terms_of_service.changelog)
diff --git a/app/views/admin/terms_of_service/index.html.haml b/app/views/admin/terms_of_service/index.html.haml
index 809d567674..636851b449 100644
--- a/app/views/admin/terms_of_service/index.html.haml
+++ b/app/views/admin/terms_of_service/index.html.haml
@@ -10,7 +10,11 @@
     .admin__terms-of-service__container__header
       .dot-indicator.success
         .dot-indicator__indicator
-        %span= t('admin.terms_of_service.live')
+        %span
+          - if @terms_of_service.effective? || @terms_of_service.effective_date.nil?
+            = t('admin.terms_of_service.live')
+          - else
+            = t('admin.terms_of_service.going_live_on_html', date: tag.time(l(@terms_of_service.effective_date), class: 'formatted', date: @terms_of_service.effective_date.iso8601))
       ·
       %span
         = t('admin.terms_of_service.published_on_html', date: tag.time(l(@terms_of_service.published_at.to_date), class: 'formatted', date: @terms_of_service.published_at.to_date.iso8601))
diff --git a/app/views/admin/trends/tags/_tag.html.haml b/app/views/admin/trends/tags/_tag.html.haml
index 1686c46652..91a484d46c 100644
--- a/app/views/admin/trends/tags/_tag.html.haml
+++ b/app/views/admin/trends/tags/_tag.html.haml
@@ -13,7 +13,7 @@
 
       - if tag.trendable?
         ·
-        %abbr{ title: t('admin.trends.tags.current_score', score: tag.trend.score) }= t('admin.trends.tags.trending_rank', rank: tag.trend.rank + 1)
+        %abbr{ title: t('admin.trends.tags.current_score', score: tag.trend.score) }= t('admin.trends.tags.trending_rank', rank: tag.trend.rank)
 
         - if tag.decaying?
           ·
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index 74dffe83ee..ebbef00ff3 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -21,20 +21,19 @@
     = f.simple_fields_for :account do |ff|
       = ff.input :username,
                  append: "@#{site_hostname}",
-                 input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT },
-                 label: false,
+                 input_html: { autocomplete: 'off', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT, placeholder: ' ' },
                  required: true,
                  wrapper: :with_label
     = f.input :email,
               hint: false,
-              input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' },
-              placeholder: t('simple_form.labels.defaults.email'),
-              required: true
+              input_html: { autocomplete: 'username', placeholder: ' ' },
+              required: true,
+              wrapper: :with_label
     = f.input :password,
               hint: false,
-              input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last },
-              placeholder: t('simple_form.labels.defaults.password'),
-              required: true
+              input_html: { autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last, placeholder: ' ' },
+              required: true,
+              wrapper: :with_label
     = f.input :password_confirmation,
               hint: false,
               input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password', maxlength: User.password_length.last },
@@ -53,6 +52,14 @@
               required: false,
               wrapper: :with_label
 
+  - if Setting.min_age.present?
+    .fields-group
+      = f.input :date_of_birth,
+                as: :date_of_birth,
+                hint: t('simple_form.hints.user.date_of_birth', age: Setting.min_age.to_i),
+                required: true,
+                wrapper: :with_block_label
+
   - if approved_registrations? && @invite.blank?
     %p.lead= t('auth.sign_up.manual_review', domain: site_hostname)
 
diff --git a/app/views/auth/shared/_links.html.haml b/app/views/auth/shared/_links.html.haml
index bd2245e111..e204fb9ce6 100644
--- a/app/views/auth/shared/_links.html.haml
+++ b/app/views/auth/shared/_links.html.haml
@@ -11,7 +11,7 @@
     - if controller_name != 'passwords' && controller_name != 'registrations'
       %li= link_to t('auth.forgot_password'), new_user_password_path
 
-    - if controller_name != 'passwords' && controller_name != 'registrations' && params[:with_options].nil?
+    - if controller_name != 'passwords' && controller_name != 'registrations' && params[:with_options].blank?
       %li= link_to t('auth.with_login_options'), new_user_session_path(with_options: '1')
 
   - if controller_name != 'confirmations' && (!user_signed_in? || !current_user.confirmed? || current_user.unconfirmed_email.present?)
diff --git a/app/views/custom_css/show_system.css.erb b/app/views/custom_css/show_system.css.erb
index 72f0281aa1..e69de29bb2 100644
--- a/app/views/custom_css/show_system.css.erb
+++ b/app/views/custom_css/show_system.css.erb
@@ -1,6 +0,0 @@
-<%- @user_roles.each do |role| %>
-.user-role-<%= role.id %> {
-  --user-role-accent: <%= role.color %>;
-}
-
-<%- end %>
diff --git a/app/views/filters/_filter_fields.html.haml b/app/views/filters/_filter_fields.html.haml
index 911b10467a..18e9a6580e 100644
--- a/app/views/filters/_filter_fields.html.haml
+++ b/app/views/filters/_filter_fields.html.haml
@@ -26,7 +26,7 @@
 .fields-group
   = f.input :filter_action,
             as: :radio_buttons,
-            collection: %i(warn hide),
+            collection: %i(warn blur hide),
             hint: t('simple_form.hints.filters.action'),
             include_blank: false,
             label_method: ->(action) { filter_action_label(action) },
diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml
index 7751337cb0..507c2e8615 100644
--- a/app/views/statuses/show.html.haml
+++ b/app/views/statuses/show.html.haml
@@ -16,6 +16,8 @@
   = opengraph 'og:title', "#{display_name(@account)} (#{acct(@account)})"
   = opengraph 'og:url', short_account_status_url(@account, @status)
   = opengraph 'og:published_time', @status.created_at.iso8601
+  - if @status.language.present?
+    = opengraph 'og:locale', @status.language
   = opengraph 'profile:username', acct(@account)[1..]
 
   = render 'og_description', activity: @status
diff --git a/app/views/system_css/show.css.erb b/app/views/system_css/show.css.erb
index 72f0281aa1..e69de29bb2 100644
--- a/app/views/system_css/show.css.erb
+++ b/app/views/system_css/show.css.erb
@@ -1,6 +0,0 @@
-<%- @user_roles.each do |role| %>
-.user-role-<%= role.id %> {
-  --user-role-accent: <%= role.color %>;
-}
-
-<%- end %>
diff --git a/app/views/user_mailer/announcement_published.html.haml b/app/views/user_mailer/announcement_published.html.haml
new file mode 100644
index 0000000000..1a879e47c9
--- /dev/null
+++ b/app/views/user_mailer/announcement_published.html.haml
@@ -0,0 +1,12 @@
+= content_for :heading do
+  = render 'application/mailer/heading',
+           image_url: frontend_asset_url('images/mailer-new/heading/user.png'),
+           title: t('user_mailer.announcement_published.title', domain: site_hostname)
+%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
+  %tr
+    %td.email-body-padding-td
+      %table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
+        %tr
+          %td.email-inner-card-td.email-prose
+            %p= t('user_mailer.announcement_published.description', domain: site_hostname)
+            = linkify(@announcement.text)
diff --git a/app/views/user_mailer/announcement_published.text.erb b/app/views/user_mailer/announcement_published.text.erb
new file mode 100644
index 0000000000..94bb4029dc
--- /dev/null
+++ b/app/views/user_mailer/announcement_published.text.erb
@@ -0,0 +1,7 @@
+<%= t('user_mailer.announcement_published.title') %>
+
+===
+
+<%= t('user_mailer.announcement_published.description', domain: site_hostname) %>
+
+<%= @announcement.text %>
diff --git a/app/views/user_mailer/terms_of_service_changed.html.haml b/app/views/user_mailer/terms_of_service_changed.html.haml
index 95cc976418..2e34eb4990 100644
--- a/app/views/user_mailer/terms_of_service_changed.html.haml
+++ b/app/views/user_mailer/terms_of_service_changed.html.haml
@@ -9,7 +9,7 @@
       %table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
         %tr
           %td.email-inner-card-td.email-prose
-            %p= t('user_mailer.terms_of_service_changed.description_html', path: terms_of_service_url, domain: site_hostname)
+            %p= t('user_mailer.terms_of_service_changed.description_html', path: terms_of_service_version_url(date: @terms_of_service.effective_date), domain: site_hostname, date: l(@terms_of_service.effective_date || Time.zone.today))
             %p
               %strong= t('user_mailer.terms_of_service_changed.changelog')
             = markdown(@terms_of_service.changelog)
diff --git a/app/views/user_mailer/terms_of_service_changed.text.erb b/app/views/user_mailer/terms_of_service_changed.text.erb
index 8416572f0a..ccf332ce89 100644
--- a/app/views/user_mailer/terms_of_service_changed.text.erb
+++ b/app/views/user_mailer/terms_of_service_changed.text.erb
@@ -2,9 +2,9 @@
 
 ===
 
-<%= t('user_mailer.terms_of_service_changed.description', domain: site_hostname) %>
+<%= t('user_mailer.terms_of_service_changed.description', domain: site_hostname, date: l(@terms_of_service.effective_date || Time.zone.today)) %>
 
-=> <%= terms_of_service_url %>
+=> <%= terms_of_service_version_url(date: @terms_of_service.effective_date) %>
 
 <%= t('user_mailer.terms_of_service_changed.changelog') %>
 
diff --git a/app/workers/activitypub/fetch_all_replies_worker.rb b/app/workers/activitypub/fetch_all_replies_worker.rb
new file mode 100644
index 0000000000..849a06d0fa
--- /dev/null
+++ b/app/workers/activitypub/fetch_all_replies_worker.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+# Fetch all replies to a status, querying recursively through
+# ActivityPub replies collections, fetching any statuses that
+# we either don't already have or we haven't checked for new replies
+# in the Status::FETCH_REPLIES_COOLDOWN_MINUTES interval
+class ActivityPub::FetchAllRepliesWorker
+  include Sidekiq::Worker
+  include ExponentialBackoff
+  include JsonLdHelper
+
+  sidekiq_options queue: 'pull', retry: 3
+
+  # Global max replies to fetch per request (all replies, recursively)
+  MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_GLOBAL'] || 1000).to_i
+  MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i
+
+  def perform(root_status_id, options = {})
+    @root_status = Status.remote.find_by(id: root_status_id)
+    return unless @root_status&.should_fetch_replies?
+
+    @root_status.touch(:fetched_replies_at)
+    Rails.logger.debug { "FetchAllRepliesWorker - #{@root_status.uri}: Fetching all replies for status: #{@root_status}" }
+
+    uris_to_fetch, n_pages = get_replies(@root_status.uri, MAX_PAGES, options)
+    return if uris_to_fetch.nil?
+
+    fetched_uris = uris_to_fetch.clone.to_set
+
+    until uris_to_fetch.empty? || fetched_uris.length >= MAX_REPLIES || n_pages >= MAX_PAGES
+      next_reply = uris_to_fetch.pop
+      next if next_reply.nil?
+
+      new_reply_uris, new_n_pages = get_replies(next_reply, MAX_PAGES - n_pages, options)
+      next if new_reply_uris.nil?
+
+      new_reply_uris = new_reply_uris.reject { |uri| fetched_uris.include?(uri) }
+
+      uris_to_fetch.concat(new_reply_uris)
+      fetched_uris = fetched_uris.merge(new_reply_uris)
+      n_pages += new_n_pages
+    end
+
+    Rails.logger.debug { "FetchAllRepliesWorker - #{@root_status.uri}: fetched #{fetched_uris.length} replies" }
+
+    # Workers shouldn't be returning anything, but this is used in tests
+    fetched_uris
+  end
+
+  private
+
+  def get_replies(status_uri, max_pages, options = {})
+    replies_collection_or_uri = get_replies_uri(status_uri)
+    return if replies_collection_or_uri.nil?
+
+    ActivityPub::FetchAllRepliesService.new.call(status_uri, replies_collection_or_uri, max_pages: max_pages, **options.deep_symbolize_keys)
+  end
+
+  def get_replies_uri(parent_status_uri)
+    fetch_resource(parent_status_uri, true)&.fetch('replies', nil)
+  rescue => e
+    Rails.logger.info { "FetchAllRepliesWorker - #{@root_status.uri}: Caught exception while resolving replies URI #{parent_status_uri}: #{e} - #{e.message}" }
+    # Raise if we can't get the collection for top-level status to trigger retry
+    raise e if parent_status_uri == @root_status.uri
+
+    nil
+  end
+end
diff --git a/app/workers/activitypub/fetch_replies_worker.rb b/app/workers/activitypub/fetch_replies_worker.rb
index d72bad7452..f9b3dbf171 100644
--- a/app/workers/activitypub/fetch_replies_worker.rb
+++ b/app/workers/activitypub/fetch_replies_worker.rb
@@ -7,7 +7,7 @@ class ActivityPub::FetchRepliesWorker
   sidekiq_options queue: 'pull', retry: 3
 
   def perform(parent_status_id, replies_uri, options = {})
-    ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id), replies_uri, **options.deep_symbolize_keys)
+    ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id).account.uri, replies_uri, **options.deep_symbolize_keys)
   rescue ActiveRecord::RecordNotFound
     true
   end
diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb
index a04ac621f3..9a418f0f3d 100644
--- a/app/workers/activitypub/update_distribution_worker.rb
+++ b/app/workers/activitypub/update_distribution_worker.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker
+  DEBOUNCE_DELAY = 5.seconds
+
   sidekiq_options queue: 'push', lock: :until_executed, lock_ttl: 1.day.to_i
 
   # Distribute an profile update to servers that might have a copy
diff --git a/app/workers/admin/distribute_announcement_notification_worker.rb b/app/workers/admin/distribute_announcement_notification_worker.rb
new file mode 100644
index 0000000000..a8b9a0bd94
--- /dev/null
+++ b/app/workers/admin/distribute_announcement_notification_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Admin::DistributeAnnouncementNotificationWorker
+  include Sidekiq::Worker
+
+  def perform(announcement_id)
+    announcement = Announcement.find(announcement_id)
+
+    announcement.scope_for_notification.find_each do |user|
+      UserMailer.announcement_published(user, announcement).deliver_later!
+    end
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb
deleted file mode 100644
index 2298b095a7..0000000000
--- a/app/workers/import/relationship_worker.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This is a deprecated worker, only kept to not break ongoing imports
-# on upgrade. See `Import::RowWorker` for its replacement.
-
-class Import::RelationshipWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'pull', retry: 8, dead: false
-
-  def perform(account_id, target_account_uri, relationship, options)
-    from_account   = Account.find(account_id)
-    target_domain  = domain(target_account_uri)
-    target_account = stoplight_wrapper(target_domain).run { ResolveAccountService.new.call(target_account_uri, { check_delivery_availability: true }) }
-    options.symbolize_keys!
-
-    return if target_account.nil?
-
-    case relationship
-    when 'follow'
-      begin
-        FollowService.new.call(from_account, target_account, **options)
-      rescue ActiveRecord::RecordInvalid
-        raise if FollowLimitValidator.limit_for_account(from_account) < from_account.following_count
-      end
-    when 'unfollow'
-      UnfollowService.new.call(from_account, target_account)
-    when 'block'
-      BlockService.new.call(from_account, target_account)
-    when 'unblock'
-      UnblockService.new.call(from_account, target_account)
-    when 'mute'
-      MuteService.new.call(from_account, target_account, **options)
-    when 'unmute'
-      UnmuteService.new.call(from_account, target_account)
-    end
-  rescue ActiveRecord::RecordNotFound
-    true
-  end
-
-  def domain(uri)
-    domain = uri.is_a?(Account) ? uri.domain : uri.split('@')[1]
-    TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain)
-  end
-
-  def stoplight_wrapper(domain)
-    if domain.present?
-      Stoplight("source:#{domain}")
-        .with_fallback { nil }
-        .with_threshold(1)
-        .with_cool_off_time(5.minutes.seconds)
-        .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
-    else
-      Stoplight('domain-blank')
-    end
-  end
-end
diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb
deleted file mode 100644
index b6afb972a9..0000000000
--- a/app/workers/import_worker.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This is a deprecated worker, only kept to not break ongoing imports
-# on upgrade. See `ImportWorker` for its replacement.
-
-class ImportWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'pull', retry: false
-
-  def perform(import_id)
-    import = Import.find(import_id)
-    ImportService.new.call(import)
-  ensure
-    import&.destroy
-  end
-end
diff --git a/app/workers/poll_expiration_notify_worker.rb b/app/workers/poll_expiration_notify_worker.rb
index b7a60fab84..fe7647024e 100644
--- a/app/workers/poll_expiration_notify_worker.rb
+++ b/app/workers/poll_expiration_notify_worker.rb
@@ -8,7 +8,7 @@ class PollExpirationNotifyWorker
   def perform(poll_id)
     @poll = Poll.find(poll_id)
 
-    return if does_not_expire?
+    return if missing_expiration?
     requeue! && return if not_due_yet?
 
     notify_remote_voters_and_owner! if @poll.local?
@@ -24,7 +24,7 @@ class PollExpirationNotifyWorker
 
   private
 
-  def does_not_expire?
+  def missing_expiration?
     @poll.expires_at.nil?
   end
 
diff --git a/app/workers/unfilter_notifications_worker.rb b/app/workers/unfilter_notifications_worker.rb
index 53a35ce12c..cb8a46b8f4 100644
--- a/app/workers/unfilter_notifications_worker.rb
+++ b/app/workers/unfilter_notifications_worker.rb
@@ -4,25 +4,14 @@ class UnfilterNotificationsWorker
   include Sidekiq::Worker
   include Redisable
 
-  # Earlier versions of the feature passed a `notification_request` ID
-  # If `to_account_id` is passed, the first argument is an account ID
-  # TODO for after 4.3.0: drop the single-argument case
-  def perform(notification_request_or_account_id, from_account_id = nil)
-    if from_account_id.present?
-      @notification_request = nil
-      @from_account = Account.find_by(id: from_account_id)
-      @recipient    = Account.find_by(id: notification_request_or_account_id)
-    else
-      @notification_request = NotificationRequest.find_by(id: notification_request_or_account_id)
-      @from_account = @notification_request&.from_account
-      @recipient    = @notification_request&.account
-    end
+  def perform(account_id, from_account_id)
+    @from_account = Account.find_by(id: from_account_id)
+    @recipient    = Account.find_by(id: account_id)
 
     return if @from_account.nil? || @recipient.nil?
 
     push_to_conversations!
     unfilter_notifications!
-    remove_request!
     decrement_worker_count!
   end
 
@@ -36,10 +25,6 @@ class UnfilterNotificationsWorker
     filtered_notifications.in_batches.update_all(filtered: false)
   end
 
-  def remove_request!
-    @notification_request&.destroy!
-  end
-
   def filtered_notifications
     Notification.where(account: @recipient, from_account: @from_account, filtered: true)
   end
diff --git a/config/application.rb b/config/application.rb
index 92976e87ab..ae960f8b24 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -34,10 +34,11 @@ require_relative '../lib/paperclip/transcoder'
 require_relative '../lib/paperclip/type_corrector'
 require_relative '../lib/paperclip/response_with_limit_adapter'
 require_relative '../lib/terrapin/multi_pipe_extensions'
+require_relative '../lib/mastodon/middleware/public_file_server'
+require_relative '../lib/mastodon/middleware/socket_cleanup'
+require_relative '../lib/mastodon/feature'
 require_relative '../lib/mastodon/snowflake'
 require_relative '../lib/mastodon/version'
-require_relative '../lib/mastodon/rack_middleware'
-require_relative '../lib/public_file_server_middleware'
 require_relative '../lib/devise/strategies/two_factor_ldap_authenticatable'
 require_relative '../lib/devise/strategies/two_factor_pam_authenticatable'
 require_relative '../lib/elasticsearch/client_extensions'
@@ -87,9 +88,9 @@ module Mastodon
     # We use our own middleware for this
     config.public_file_server.enabled = false
 
-    config.middleware.use PublicFileServerMiddleware if Rails.env.local? || ENV['RAILS_SERVE_STATIC_FILES'] == 'true'
+    config.middleware.use Mastodon::Middleware::PublicFileServer if Rails.env.local? || ENV['RAILS_SERVE_STATIC_FILES'] == 'true'
     config.middleware.use Rack::Attack
-    config.middleware.use Mastodon::RackMiddleware
+    config.middleware.use Mastodon::Middleware::SocketCleanup
 
     config.before_configuration do
       require 'mastodon/redis_configuration'
diff --git a/config/initializers/0_post_deployment_migrations.rb b/config/initializers/0_post_deployment_migrations.rb
index 8e4d63a2e5..7af678b456 100644
--- a/config/initializers/0_post_deployment_migrations.rb
+++ b/config/initializers/0_post_deployment_migrations.rb
@@ -4,14 +4,4 @@
 # before other initializers as Rails may otherwise memoize a list of migrations
 # excluding the post deployment migrations.
 
-unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
-  Rails.application.config.paths['db'].each do |db_path|
-    path = Rails.root.join(db_path, 'post_migrate').to_s
-
-    Rails.application.config.paths['db/migrate'] << path
-
-    # Rails memoizes migrations at certain points where it won't read the above
-    # path just yet. As such we must also update the following list of paths.
-    ActiveRecord::Migrator.migrations_paths << path
-  end
-end
+Mastodon::Database.add_post_migrate_path_to_rails
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index ed16d50a76..6d908fa477 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -169,7 +169,7 @@ else
 end
 
 Rails.application.reloader.to_prepare do
-  Paperclip.options[:content_type_mappings] = { csv: Import::FILE_TYPES }
+  Paperclip.options[:content_type_mappings] = { csv: %w(text/plain text/csv application/csv) }
 end
 
 # In some places in the code, we rescue this exception, but we don't always
diff --git a/config/initializers/prometheus_exporter.rb b/config/initializers/prometheus_exporter.rb
index 197777e3b6..fdfee59dc8 100644
--- a/config/initializers/prometheus_exporter.rb
+++ b/config/initializers/prometheus_exporter.rb
@@ -1,6 +1,9 @@
 # frozen_string_literal: true
 
 if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true'
+  require 'prometheus_exporter'
+  require 'prometheus_exporter/middleware'
+
   if ENV['MASTODON_PROMETHEUS_EXPORTER_LOCAL'] == 'true'
     require 'prometheus_exporter/server'
     require 'prometheus_exporter/client'
@@ -16,9 +19,11 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true'
 
   if ENV['MASTODON_PROMETHEUS_EXPORTER_WEB_DETAILED_METRICS'] == 'true'
     # Optional, as those metrics might generate extra overhead and be redundant with what OTEL provides
-    require 'prometheus_exporter/middleware'
-
     # Per-action/controller request stats like HTTP status and timings
     Rails.application.middleware.unshift PrometheusExporter::Middleware
+  else
+    # Include stripped down version of PrometheusExporter::Middleware that only collects queue time
+    require 'mastodon/middleware/prometheus_queue_time'
+    Rails.application.middleware.unshift Mastodon::Middleware::PrometheusQueueTime, instrument: false
   end
 end
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index b4eaab1daa..f558ee5fe0 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -122,7 +122,7 @@ class Rack::Attack
   end
 
   throttle('throttle_email_confirmations/ip', limit: 25, period: 5.minutes) do |req|
-    req.throttleable_remote_ip if req.post? && (req.path_matches?('/auth/confirmation') || req.path == '/api/v1/emails/confirmations')
+    req.throttleable_remote_ip if (req.post? && (req.path_matches?('/auth/confirmation') || req.path == '/api/v1/emails/confirmations')) || ((req.put? || req.patch?) && req.path_matches?('/auth/setup'))
   end
 
   throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req|
@@ -133,6 +133,14 @@ class Rack::Attack
     end
   end
 
+  throttle('throttle_auth_setup/email', limit: 5, period: 10.minutes) do |req|
+    req.params.dig('user', 'email').presence if (req.put? || req.patch?) && req.path_matches?('/auth/setup')
+  end
+
+  throttle('throttle_auth_setup/account', limit: 5, period: 10.minutes) do |req|
+    req.warden_user_id if (req.put? || req.patch?) && req.path_matches?('/auth/setup')
+  end
+
   throttle('throttle_login_attempts/ip', limit: 25, period: 5.minutes) do |req|
     req.throttleable_remote_ip if req.post? && req.path_matches?('/auth/sign_in')
   end
diff --git a/config/initializers/webauthn.rb b/config/initializers/webauthn.rb
index 40dfeb8317..ad8af3876c 100644
--- a/config/initializers/webauthn.rb
+++ b/config/initializers/webauthn.rb
@@ -3,7 +3,7 @@
 WebAuthn.configure do |config|
   # This value needs to match `window.location.origin` evaluated by
   # the User Agent during registration and authentication ceremonies.
-  config.origin = "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}"
+  config.allowed_origins = ["#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}"]
 
   # Relying Party name for display purposes
   config.rp_name = 'Mastodon'
diff --git a/config/locales/activerecord.bg.yml b/config/locales/activerecord.bg.yml
index 1a3f71a9cd..0b33e953ae 100644
--- a/config/locales/activerecord.bg.yml
+++ b/config/locales/activerecord.bg.yml
@@ -49,8 +49,14 @@ bg:
           attributes:
             reblog:
               taken: от публикациите вече съществуват
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: е твърде скоро и трябва да е по-късно от %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: е под възрастовата граница
             email:
               blocked: използва се забранен доставчик на услуга за е-поща
               unreachable: изглежда не съществува
diff --git a/config/locales/activerecord.ca.yml b/config/locales/activerecord.ca.yml
index 23b1d43544..f53f7f364a 100644
--- a/config/locales/activerecord.ca.yml
+++ b/config/locales/activerecord.ca.yml
@@ -49,8 +49,14 @@ ca:
           attributes:
             reblog:
               taken: de la publicació ja existeix
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: és massa aviat, ha de ser després de %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: és inferior a l'edat mínima
             email:
               blocked: utilitza un proveïdor de correu-e no autoritzat
               unreachable: sembla que no existeix
diff --git a/config/locales/activerecord.cs.yml b/config/locales/activerecord.cs.yml
index 8aa1848ca1..38708713d2 100644
--- a/config/locales/activerecord.cs.yml
+++ b/config/locales/activerecord.cs.yml
@@ -49,8 +49,14 @@ cs:
           attributes:
             reblog:
               taken: příspěvku již existuje
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: je příliš brzy, musí být později než %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: je pod věkovou hranicí
             email:
               blocked: používá zakázanou e-mailovou službu
               unreachable: pravděpodobně neexistuje
diff --git a/config/locales/activerecord.cy.yml b/config/locales/activerecord.cy.yml
index 526d3f646f..c201016be6 100644
--- a/config/locales/activerecord.cy.yml
+++ b/config/locales/activerecord.cy.yml
@@ -3,7 +3,7 @@ cy:
   activerecord:
     attributes:
       poll:
-        expires_at: Terfyn amser
+        expires_at: Dyddiad cau
         options: Dewisiadau
       user:
         agreement: Cytundeb gwasanaeth
@@ -49,8 +49,14 @@ cy:
           attributes:
             reblog:
               taken: o'r statws yn bodoli'n barod
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: yn rhy fuan, rhaid iddo fod yn hwyrach na %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: yn iau na'r terfyn oedran
             email:
               blocked: yn defnyddio darparwr e-bost nad yw'n cael ei ganiatáu
               unreachable: nid yw i weld yn bodoli
@@ -59,7 +65,7 @@ cy:
         user_role:
           attributes:
             permissions_as_keys:
-              dangerous: yn cynnwys caniatâd nad ydynt yn ddiogel ar gyfer rôl sail
+              dangerous: yn cynnwys caniatâd nad ydyn nhw'n ddiogel ar gyfer rôl sail
               elevated: yn methu a chynnwys caniatâd nad yw eich rôl cyfredol yn ei gynnwys
               own_role: nid oes modd ei newid gyda'ch rôl cyfredol
             position:
@@ -68,4 +74,4 @@ cy:
         webhook:
           attributes:
             events:
-              invalid_permissions: ni ellir cynnwys digwyddiadau nad oes gennych yr hawl iddynt
+              invalid_permissions: nid oes modd cynnwys digwyddiadau nad oes gennych yr hawl iddyn nhw
diff --git a/config/locales/activerecord.da.yml b/config/locales/activerecord.da.yml
index 0c1eb5e6dd..7b49c18ca3 100644
--- a/config/locales/activerecord.da.yml
+++ b/config/locales/activerecord.da.yml
@@ -49,8 +49,14 @@ da:
           attributes:
             reblog:
               taken: af status findes allerede
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: er for tidligt, skal være efter %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: er under alderskravet
             email:
               blocked: bruger en ikke-tilladt e-mailudbyder
               unreachable: ser ikke ud til at eksistere
diff --git a/config/locales/activerecord.de.yml b/config/locales/activerecord.de.yml
index b20e6a65f8..4ae7aec5dd 100644
--- a/config/locales/activerecord.de.yml
+++ b/config/locales/activerecord.de.yml
@@ -49,8 +49,14 @@ de:
           attributes:
             reblog:
               taken: des Beitrags existiert bereits
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: Datum muss später als %{date} sein
         user:
           attributes:
+            date_of_birth:
+              below_limit: liegt unterhalb der Altersgrenze
             email:
               blocked: verwendet einen unerlaubten E-Mail-Anbieter
               unreachable: scheint nicht zu existieren
diff --git a/config/locales/activerecord.el.yml b/config/locales/activerecord.el.yml
index 9b17b75939..58e7a7df3f 100644
--- a/config/locales/activerecord.el.yml
+++ b/config/locales/activerecord.el.yml
@@ -49,6 +49,10 @@ el:
           attributes:
             reblog:
               taken: της ανάρτησης υπάρχει ήδη
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: είναι πολύ σύντομα, πρέπει να είναι μετά από %{date}
         user:
           attributes:
             email:
diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml
index f10a9011b3..6940d589ca 100644
--- a/config/locales/activerecord.en.yml
+++ b/config/locales/activerecord.en.yml
@@ -49,8 +49,14 @@ en:
           attributes:
             reblog:
               taken: of post already exists
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: is too soon, must be later than %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: is below the age limit
             email:
               blocked: uses a disallowed e-mail provider
               unreachable: does not seem to exist
diff --git a/config/locales/activerecord.eo.yml b/config/locales/activerecord.eo.yml
index fcf5bb3160..57012564be 100644
--- a/config/locales/activerecord.eo.yml
+++ b/config/locales/activerecord.eo.yml
@@ -49,6 +49,10 @@ eo:
           attributes:
             reblog:
               taken: de afiŝo jam ekzistas
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: estas tro frue, devas esti pli malfrue ol %{date}
         user:
           attributes:
             email:
diff --git a/config/locales/activerecord.es-AR.yml b/config/locales/activerecord.es-AR.yml
index 5a74092458..62a409a353 100644
--- a/config/locales/activerecord.es-AR.yml
+++ b/config/locales/activerecord.es-AR.yml
@@ -49,8 +49,14 @@ es-AR:
           attributes:
             reblog:
               taken: del mensaje ya existe
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: es demasiado pronto, debe ser posterior a %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: está por debajo de la edad mínima
             email:
               blocked: usa un proveedor de correo electrónico no permitido
               unreachable: no parece existir
diff --git a/config/locales/activerecord.es-MX.yml b/config/locales/activerecord.es-MX.yml
index 4f080eca8c..02384f1c71 100644
--- a/config/locales/activerecord.es-MX.yml
+++ b/config/locales/activerecord.es-MX.yml
@@ -49,8 +49,14 @@ es-MX:
           attributes:
             reblog:
               taken: de la publicación ya existe
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: es demasiado pronto, debe ser posterior al %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: está por debajo del límite de edad
             email:
               blocked: utiliza un proveedor de correo no autorizado
               unreachable: no parece existir
diff --git a/config/locales/activerecord.es.yml b/config/locales/activerecord.es.yml
index 60d52ad8f2..94f29365e9 100644
--- a/config/locales/activerecord.es.yml
+++ b/config/locales/activerecord.es.yml
@@ -49,8 +49,14 @@ es:
           attributes:
             reblog:
               taken: de la publicación ya existe
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: es demasiado pronto, debe ser posterior a %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: está por debajo de la edad mínima
             email:
               blocked: utiliza un proveedor de correo no autorizado
               unreachable: no parece existir
diff --git a/config/locales/activerecord.fa.yml b/config/locales/activerecord.fa.yml
index 6af1a42e27..5db02e26c7 100644
--- a/config/locales/activerecord.fa.yml
+++ b/config/locales/activerecord.fa.yml
@@ -49,6 +49,10 @@ fa:
           attributes:
             reblog:
               taken: تا از وضعیت‌ها هنوز وجود دارند
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: بیش از حد زود است. باید بعد از %{date} باشد
         user:
           attributes:
             email:
diff --git a/config/locales/activerecord.fi.yml b/config/locales/activerecord.fi.yml
index 619f2cd584..c731688a1f 100644
--- a/config/locales/activerecord.fi.yml
+++ b/config/locales/activerecord.fi.yml
@@ -44,13 +44,19 @@ fi:
           attributes:
             account_id:
               taken: on jo listassa
-          must_be_following: on oltava seurattu tili
+          must_be_following: on oltava seurattava tili
         status:
           attributes:
             reblog:
               taken: tästä julkaisusta on jo tehty
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: on liian pian, täytyy olla myöhemmin kuin %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: alittaa alaikärajan
             email:
               blocked: käyttää kiellettyä sähköpostipalveluntarjoajaa
               unreachable: ei näytä olevan olemassa
diff --git a/config/locales/activerecord.fo.yml b/config/locales/activerecord.fo.yml
index 054a828755..ce84a1fffd 100644
--- a/config/locales/activerecord.fo.yml
+++ b/config/locales/activerecord.fo.yml
@@ -49,8 +49,14 @@ fo:
           attributes:
             reblog:
               taken: frá posti sum longu finst
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: tað er ov tíðliga, má vera eftir %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: er niðanfyri aldursmarkið
             email:
               blocked: brúkar ein ikki loyvdan teldopostveitara
               unreachable: tykist ikki at vera til
diff --git a/config/locales/activerecord.fr-CA.yml b/config/locales/activerecord.fr-CA.yml
index 97aee2304c..a966bb5a8a 100644
--- a/config/locales/activerecord.fr-CA.yml
+++ b/config/locales/activerecord.fr-CA.yml
@@ -49,8 +49,14 @@ fr-CA:
           attributes:
             reblog:
               taken: de la publication existe déjà
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: est trop tôt, doit être plus tard que %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: est en dessous de la limite d'âge
             email:
               blocked: utilise un fournisseur de courriel interdit
               unreachable: ne semble pas exister
diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml
index 85dbaf293b..ae3ce7f9cb 100644
--- a/config/locales/activerecord.fr.yml
+++ b/config/locales/activerecord.fr.yml
@@ -49,8 +49,14 @@ fr:
           attributes:
             reblog:
               taken: du message existe déjà
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: est trop tôt, doit être plus tard que %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: est en dessous de la limite d'âge
             email:
               blocked: utilise un fournisseur de courriel interdit
               unreachable: ne semble pas exister
diff --git a/config/locales/activerecord.ga.yml b/config/locales/activerecord.ga.yml
index c0b86482ce..853c705663 100644
--- a/config/locales/activerecord.ga.yml
+++ b/config/locales/activerecord.ga.yml
@@ -49,8 +49,14 @@ ga:
           attributes:
             reblog:
               taken: den phost cheana féin
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: róluath, caithfidh sé bheith níos déanaí ná %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: faoi ​​bhun na teorann aoise
             email:
               blocked: úsáideann soláthraí ríomhphoist dícheadaithe
               unreachable: ní cosúil go bhfuil sé ann
diff --git a/config/locales/activerecord.gl.yml b/config/locales/activerecord.gl.yml
index 92cbcc5fb4..f4e6725565 100644
--- a/config/locales/activerecord.gl.yml
+++ b/config/locales/activerecord.gl.yml
@@ -7,7 +7,7 @@ gl:
         options: Opcións
       user:
         agreement: Acordo do Servizo
-        email: Enderezo de email
+        email: Enderezo de correo
         locale: Locale
         password: Contrasinal
       user/account:
@@ -49,8 +49,14 @@ gl:
           attributes:
             reblog:
               taken: do estado xa existe
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: é demasiado axiña, debería ser posterior a %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: é inferior ao límite de idade
             email:
               blocked: utiliza un provedor de email non autorizado
               unreachable: semella que non existe
diff --git a/config/locales/activerecord.he.yml b/config/locales/activerecord.he.yml
index 57f5e4bcbb..7dff17493b 100644
--- a/config/locales/activerecord.he.yml
+++ b/config/locales/activerecord.he.yml
@@ -49,8 +49,14 @@ he:
           attributes:
             reblog:
               taken: של ההודעה כבר קיים
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: מוקדם מדי, חייב להיות אחרי %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: מתחת למגבלת הגיל
             email:
               blocked: עושה שימוש בספק דוא"ל אסור
               unreachable: נראה שלא קיים
diff --git a/config/locales/activerecord.hu.yml b/config/locales/activerecord.hu.yml
index b0ac003e9d..cf2f50a9f9 100644
--- a/config/locales/activerecord.hu.yml
+++ b/config/locales/activerecord.hu.yml
@@ -49,8 +49,14 @@ hu:
           attributes:
             reblog:
               taken: már létezik ehhez a bejegyzéshez
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: túl korán van, később kellene lennie, mint %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: a korhatár alatt van
             email:
               blocked: egy letiltott email szolgáltatót használ
               unreachable: úgy tűnik, hogy nem létezik
diff --git a/config/locales/activerecord.io.yml b/config/locales/activerecord.io.yml
index 8663c67218..67b59e24fc 100644
--- a/config/locales/activerecord.io.yml
+++ b/config/locales/activerecord.io.yml
@@ -23,6 +23,8 @@ io:
       models:
         account:
           attributes:
+            fields:
+              fields_with_values_missing_labels: enhavas valori kun mankanta etiketi
             username:
               invalid: mustas konsistar nur literi, nombri e sublinei
               reserved: rezervitas
@@ -38,6 +40,11 @@ io:
           attributes:
             data:
               malformed: esas misformaca
+        list_account:
+          attributes:
+            account_id:
+              taken: es ja en la listo
+          must_be_following: mustas esar sequinta konto
         status:
           attributes:
             reblog:
diff --git a/config/locales/activerecord.is.yml b/config/locales/activerecord.is.yml
index 2f4801ac9c..cff90a3476 100644
--- a/config/locales/activerecord.is.yml
+++ b/config/locales/activerecord.is.yml
@@ -49,8 +49,14 @@ is:
           attributes:
             reblog:
               taken: af færslum er þegar fyrirliggjandi
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: er of snemmt, verður að vera síðar en %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: er undir aldurstakmörkum
             email:
               blocked: notar óleyfilega tölvupóstþjónustu
               unreachable: virðist ekki vera til
diff --git a/config/locales/activerecord.it.yml b/config/locales/activerecord.it.yml
index 2b408f548e..9ff385f26e 100644
--- a/config/locales/activerecord.it.yml
+++ b/config/locales/activerecord.it.yml
@@ -49,8 +49,14 @@ it:
           attributes:
             reblog:
               taken: del post esiste già
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: è troppo presto, deve essere successivo alla data %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: è inferiore al limite di età
             email:
               blocked: utilizza un provider di posta elettronica non autorizzato
               unreachable: non sembra esistere
diff --git a/config/locales/activerecord.ja.yml b/config/locales/activerecord.ja.yml
index 42a107a31c..b52e92a4ec 100644
--- a/config/locales/activerecord.ja.yml
+++ b/config/locales/activerecord.ja.yml
@@ -23,6 +23,8 @@ ja:
       models:
         account:
           attributes:
+            fields:
+              fields_with_values_missing_labels: プロフィール補足情報にラベルが入力されていないものがあります
             username:
               invalid: アルファベット・数字・アンダーバーの組み合わせで入力してください
               reserved: は予約されています
diff --git a/config/locales/activerecord.ko.yml b/config/locales/activerecord.ko.yml
index 9442c3aae3..3aa991734b 100644
--- a/config/locales/activerecord.ko.yml
+++ b/config/locales/activerecord.ko.yml
@@ -49,8 +49,14 @@ ko:
           attributes:
             reblog:
               taken: 이미 게시물이 존재합니다
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: 너무 이릅니다. %{date} 이후로 지정해야 합니다
         user:
           attributes:
+            date_of_birth:
+              below_limit: 나이 제한보다 아래입니다
             email:
               blocked: 허용되지 않은 이메일 제공자입니다
               unreachable: 존재하지 않는 것 같습니다
diff --git a/config/locales/activerecord.lt.yml b/config/locales/activerecord.lt.yml
index 4f94a38082..1eec2782f4 100644
--- a/config/locales/activerecord.lt.yml
+++ b/config/locales/activerecord.lt.yml
@@ -49,8 +49,14 @@ lt:
           attributes:
             reblog:
               taken: įrašas jau egzistuoja.
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: yra per anksti, turi būti vėliau nei %{date}.
         user:
           attributes:
+            date_of_birth:
+              below_limit: yra žemiau amžiaus ribos.
             email:
               blocked: naudoja neleidžiamą el. laiško paslaugų teikėją.
               unreachable: neatrodo, kad egzistuoja.
diff --git a/config/locales/activerecord.lv.yml b/config/locales/activerecord.lv.yml
index 6d912bd628..c7030221a7 100644
--- a/config/locales/activerecord.lv.yml
+++ b/config/locales/activerecord.lv.yml
@@ -49,8 +49,14 @@ lv:
           attributes:
             reblog:
               taken: ziņai jau pastāv
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: ir pārāk agri, jābūt vēlāk kā %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: ir mazāks par minimālo vecuma ierobežojumu
             email:
               blocked: lieto neatļautu e-pasta pakalpojuma sniedzēju
               unreachable: šķietami neeksistē
diff --git a/config/locales/activerecord.nl.yml b/config/locales/activerecord.nl.yml
index 447601f0c6..b05d6680e7 100644
--- a/config/locales/activerecord.nl.yml
+++ b/config/locales/activerecord.nl.yml
@@ -49,8 +49,14 @@ nl:
           attributes:
             reblog:
               taken: van bericht bestaat al
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: is te vroeg, moet na %{date} zijn
         user:
           attributes:
+            date_of_birth:
+              below_limit: is onder de leeftijdsgrens
             email:
               blocked: gebruikt een niet toegestane e-mailprovider
               unreachable: schijnt niet te bestaan
diff --git a/config/locales/activerecord.no.yml b/config/locales/activerecord.no.yml
index 5abe3fdc38..b79a5bcccb 100644
--- a/config/locales/activerecord.no.yml
+++ b/config/locales/activerecord.no.yml
@@ -15,6 +15,9 @@
       user/invite_request:
         text: Årsak
     errors:
+      attributes:
+        domain:
+          invalid: er ikke et gyldig domenenavn
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.pt-BR.yml b/config/locales/activerecord.pt-BR.yml
index fde744bed6..8b77ed7e83 100644
--- a/config/locales/activerecord.pt-BR.yml
+++ b/config/locales/activerecord.pt-BR.yml
@@ -23,6 +23,8 @@ pt-BR:
       models:
         account:
           attributes:
+            fields:
+              fields_with_values_missing_labels: contém valores sem rótulos
             username:
               invalid: apenas letras, números e underlines ( "_" )
               reserved: já existe
@@ -37,7 +39,7 @@ pt-BR:
         import:
           attributes:
             data:
-              malformed: Está malformado
+              malformed: está malformado
         list_account:
           attributes:
             account_id:
@@ -47,13 +49,19 @@ pt-BR:
           attributes:
             reblog:
               taken: do toot já existe
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: está muito próxima, deve ser posterior a %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: está abaixo do limite de idade
             email:
               blocked: usa provedor de e-mail não permitido
               unreachable: parece não existir
             role_id:
-              elevated: não pode maior que sua função atual
+              elevated: não pode ser maior que a sua função atual
         user_role:
           attributes:
             permissions_as_keys:
diff --git a/config/locales/activerecord.pt-PT.yml b/config/locales/activerecord.pt-PT.yml
index 274a2bf01f..397ed492e5 100644
--- a/config/locales/activerecord.pt-PT.yml
+++ b/config/locales/activerecord.pt-PT.yml
@@ -49,8 +49,14 @@ pt-PT:
           attributes:
             reblog:
               taken: da publicação já existe
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: é muito cedo, deve ser após %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: está abaixo da idade mínima
             email:
               blocked: usa um fornecedor de e-mail que não é permitido
               unreachable: não parece existir
diff --git a/config/locales/activerecord.ru.yml b/config/locales/activerecord.ru.yml
index 8769212d52..08e91e459f 100644
--- a/config/locales/activerecord.ru.yml
+++ b/config/locales/activerecord.ru.yml
@@ -3,7 +3,7 @@ ru:
   activerecord:
     attributes:
       poll:
-        expires_at: Крайний срок
+        expires_at: Срок окончания голосования
         options: Варианты
       user:
         agreement: Соглашение с условиями сервиса
@@ -23,6 +23,8 @@ ru:
       models:
         account:
           attributes:
+            fields:
+              fields_with_values_missing_labels: содержит значения с отсутствующими ключами
             username:
               invalid: только буквы, цифры и символ подчёркивания
               reserved: зарезервировано
@@ -47,8 +49,14 @@ ru:
           attributes:
             reblog:
               taken: пост уже существует
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: должна быть не ранее %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: ниже возрастного ограничения
             email:
               blocked: использует запрещённого провайдера эл. почты
               unreachable: не существует
diff --git a/config/locales/activerecord.sl.yml b/config/locales/activerecord.sl.yml
index bba6ce6aad..e4c4fe598f 100644
--- a/config/locales/activerecord.sl.yml
+++ b/config/locales/activerecord.sl.yml
@@ -18,9 +18,13 @@ sl:
       attributes:
         domain:
           invalid: ni veljavno ime domene
+      messages:
+        invalid_domain_on_line: "%{value} ni veljavno ime domene"
       models:
         account:
           attributes:
+            fields:
+              fields_with_values_missing_labels: vsebuje vrednosti, ki niso kategorizirane
             username:
               invalid: samo črke, številke in podčrtaji
               reserved: je rezerviran
@@ -40,12 +44,19 @@ sl:
           attributes:
             account_id:
               taken: je že na seznamu
+          must_be_following: mora biti račun, ki mu sledite
         status:
           attributes:
             reblog:
               taken: od objave že obstajajo
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: je prekmalu, naj bo kasneje od %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: ne dosega starostne meje
             email:
               blocked: uporablja nedovoljenega ponudnika e-poštnih storitev
               unreachable: kot kaže ne obstaja
diff --git a/config/locales/activerecord.sq.yml b/config/locales/activerecord.sq.yml
index 8c22f2e0fb..2683dd014b 100644
--- a/config/locales/activerecord.sq.yml
+++ b/config/locales/activerecord.sq.yml
@@ -49,8 +49,14 @@ sq:
           attributes:
             reblog:
               taken: e gjendjes ekziston tashmë
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: është shumë herët, duhet të jetë më vonë se %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: është nën kufirin e moshave
             email:
               blocked: përdor një shërbim email të palejuar
               unreachable: s’duket se ekziston
diff --git a/config/locales/activerecord.sv.yml b/config/locales/activerecord.sv.yml
index db488200df..74b939fda4 100644
--- a/config/locales/activerecord.sv.yml
+++ b/config/locales/activerecord.sv.yml
@@ -49,8 +49,14 @@ sv:
           attributes:
             reblog:
               taken: av status finns redan
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: är för tidigt, måste vara senare än %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: är under åldersgränsen
             email:
               blocked: använder en icke tillåten e-postleverantör
               unreachable: verkar inte existera
diff --git a/config/locales/activerecord.tr.yml b/config/locales/activerecord.tr.yml
index 0b0c34fb82..db9317afa2 100644
--- a/config/locales/activerecord.tr.yml
+++ b/config/locales/activerecord.tr.yml
@@ -49,8 +49,14 @@ tr:
           attributes:
             reblog:
               taken: durum zaten var
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: çok erken, %{date} tarihinden sonra olmalıdır
         user:
           attributes:
+            date_of_birth:
+              below_limit: yaş sınırının altında
             email:
               blocked: izin verilmeyen bir e-posta sağlayıcı kullanıyor
               unreachable: mevcut gözükmüyor
diff --git a/config/locales/activerecord.tt.yml b/config/locales/activerecord.tt.yml
index e53c2341e4..87c6592b6e 100644
--- a/config/locales/activerecord.tt.yml
+++ b/config/locales/activerecord.tt.yml
@@ -5,10 +5,51 @@ tt:
       poll:
         options: Сайлаулар
       user:
-        email: Почта адресы
+        email: Эл. почта адресы
         locale: Тел
         password: Серсүз
       user/account:
         username: Кулланучы исеме
       user/invite_request:
         text: Сәбәп
+    errors:
+      attributes:
+        domain:
+          invalid: бу домен исеме гамәлдә түгел
+      messages:
+        invalid_domain_on_line: "%{value} дөрес домен исеме түгел"
+      models:
+        account:
+          attributes:
+            username:
+              invalid: хәрефләр, цифрлар һәм ассызыклау билгеләре генә ярый
+        admin/webhook:
+          attributes:
+            url:
+              invalid: рөхсәт ителгән URL түгел
+        doorkeeper/application:
+          attributes:
+            website:
+              invalid: рөхсәт ителгән URL түгел
+        import:
+          attributes:
+            data:
+              malformed: формат дөрес түгел
+        list_account:
+          attributes:
+            account_id:
+              taken: инде исемлектә
+        status:
+          attributes:
+            reblog:
+              taken: язма инде бар
+        user:
+          attributes:
+            email:
+              blocked: ярамаган эл. почта провайдерын куллана
+            role_id:
+              elevated: сезнең хәзерге ролегездән югарырак була алмый
+        user_role:
+          attributes:
+            position:
+              elevated: сезнең хәзерге ролегездән югарырак була алмый
diff --git a/config/locales/activerecord.uk.yml b/config/locales/activerecord.uk.yml
index 1ee426ae45..8b4ba1f671 100644
--- a/config/locales/activerecord.uk.yml
+++ b/config/locales/activerecord.uk.yml
@@ -51,6 +51,8 @@ uk:
               taken: цього допису вже існує
         user:
           attributes:
+            date_of_birth:
+              below_limit: менше вікової межі
             email:
               blocked: використовує не дозволенного постачальника електронної пошти
               unreachable: не існує
diff --git a/config/locales/activerecord.vi.yml b/config/locales/activerecord.vi.yml
index 8e9e6679c4..fe810d94e5 100644
--- a/config/locales/activerecord.vi.yml
+++ b/config/locales/activerecord.vi.yml
@@ -49,8 +49,14 @@ vi:
           attributes:
             reblog:
               taken: của tút đã tồn tại
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: là quá sớm, cần phải sau %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: dưới độ tuổi tối thiểu
             email:
               blocked: sử dụng dịch vụ email bị cấm
               unreachable: không tồn tại
diff --git a/config/locales/activerecord.zh-CN.yml b/config/locales/activerecord.zh-CN.yml
index 7be8621f1f..af19014cfd 100644
--- a/config/locales/activerecord.zh-CN.yml
+++ b/config/locales/activerecord.zh-CN.yml
@@ -49,8 +49,14 @@ zh-CN:
           attributes:
             reblog:
               taken: 已经被转嘟过
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: 日期太近,必须晚于 %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: 低于年龄限制
             email:
               blocked: 使用了被封禁的电子邮件提供商
               unreachable: 似乎不存在
diff --git a/config/locales/activerecord.zh-TW.yml b/config/locales/activerecord.zh-TW.yml
index 58b703b531..f8f630ba3c 100644
--- a/config/locales/activerecord.zh-TW.yml
+++ b/config/locales/activerecord.zh-TW.yml
@@ -49,8 +49,14 @@ zh-TW:
           attributes:
             reblog:
               taken: 嘟文已經存在
+        terms_of_service:
+          attributes:
+            effective_date:
+              too_soon: 太快了,必須晚於 %{date}
         user:
           attributes:
+            date_of_birth:
+              below_limit: 低於年齡要求
             email:
               blocked: 使用不被允許的電子郵件提供商
               unreachable: 該電子郵件地址似乎無法使用
diff --git a/config/locales/ast.yml b/config/locales/ast.yml
index 6c2ebbfd72..83d158766b 100644
--- a/config/locales/ast.yml
+++ b/config/locales/ast.yml
@@ -9,8 +9,8 @@ ast:
     last_active: última actividá
     nothing_here: "¡Equí nun hai nada!"
     posts:
-      one: Artículu
-      other: Artículos
+      one: Publicación
+      other: Publicaciones
     posts_tab_heading: Artículos
   admin:
     account_actions:
@@ -76,7 +76,7 @@ ast:
         destroy_announcement_html: "%{name} desanició l'anunciu «%{target}»"
         destroy_custom_emoji_html: "%{name} desanició'l fustaxe %{target}"
         destroy_domain_block_html: "%{name} desbloquió'l dominiu %{target}"
-        destroy_status_html: "%{name} quitó l'artículu de: %{target}"
+        destroy_status_html: "%{name} quitó la publicación de: %{target}"
         destroy_user_role_html: "%{name} desanició'l rol %{target}"
         disable_custom_emoji_html: "%{name} desactivó'l fustaxe «%{target}»"
         enable_custom_emoji_html: "%{name} activó'l fustaxe «%{target}»"
@@ -87,7 +87,7 @@ ast:
         unblock_email_account_html: "%{name} desbloquió la direición de corréu electrónicu de: %{target}"
         update_announcement_html: "%{name} anovó l'anunciu «%{target}»"
         update_custom_emoji_html: "%{name} anovó'l fustaxe «%{target}»"
-        update_status_html: "%{name} anovó l'artículu de: %{target}"
+        update_status_html: "%{name} anovó la publicación de: %{target}"
         update_user_role_html: "%{name} camudó'l rol %{target}"
       empty: Nun s'atopó nengún rexistru.
     announcements:
@@ -100,7 +100,7 @@ ast:
         create: Crear l'anunciu
         title: Anunciu nuevu
       publish: Espublizar
-      published_msg: "¡L'anunciu espublizóse correutamente!"
+      published_msg: "¡L'anunciu publicóse correutamente!"
       scheduled_msg: "¡Programóse l'espublizamientu del anunciu!"
       title: Anuncios
       unpublish: Dexar d'espublizar
@@ -347,14 +347,14 @@ ast:
       back_to_account: Volver a la páxina de la cuenta
       language: Llingua
       metadata: Metadatos
-      original_status: Artículu orixinal
+      original_status: Publicación orixinal
       visibility: Visibilidá
       with_media: Con elementos multimedia
     strikes:
       actions:
-        delete_statuses: "%{name} desanició l'artículu de: %{target}"
+        delete_statuses: "%{name} desanició la publicación de: %{target}"
         disable: "%{name} conxeló la cuenta de: %{target}"
-        mark_statuses_as_sensitive: "%{name} marcó l'artículu de %{target} como sensible"
+        mark_statuses_as_sensitive: "%{name} marcó la publicación de %{target} como sensible"
         none: "%{name} unvió una alvertencia a %{target}"
         sensitive: "%{name} marcó la cuenta de %{target} como sensible"
         suspend: "%{name} suspendió la cuenta de: %{target}"
@@ -375,8 +375,8 @@ ast:
       preview_card_providers:
         title: Espublizadores
       statuses:
-        allow: Permitir l'artículu
-        disallow: Refugar l'artículu
+        allow: Permitir la publicación
+        disallow: Refugar la publicación
         title: Artículos en tendencia
       tags:
         current_score: 'Puntuación total: %{score}'
@@ -459,6 +459,10 @@ ast:
     logout: Zarrar la sesión
     migrate_account: Cambéu de cuenta
     migrate_account_html: Si quies redirixir esta cuenta a otra diferente, pues <a href="%{path}">configurar esta opción equí</a>.
+    progress:
+      confirm: Confirmación del corréu electrónicu
+      details: Los tos detalles
+      rules: Aceptación de normes
     providers:
       cas: CAS
       saml: SAML
@@ -470,6 +474,8 @@ ast:
     security: Seguranza
     setup:
       link_not_received: "¿Nun consiguiesti l'enllaz?"
+    sign_in:
+      preamble_html: Anicia la sesión coles credenciales del dominiu <strong>%{domain}</strong>. Si la to cuenta ta agospiada n'otru sirvidor, nun vas ser a aniciar la sesión equí.
     sign_up:
       title: 'Creación d''una cuenta en: %{domain}.'
     status:
@@ -515,8 +521,8 @@ ast:
       created_at: Data
       recipient: Dirixóse a
       reject_appeal: Refugar l'apellación
-      status: 'Artículu #%{id}'
-      status_removed: L'artículu yá se quitó del sistema
+      status: 'Publicación #%{id}'
+      status_removed: La publicación yá se quitó del sistema
       your_appeal_approved: Aprobóse la to apellación
       your_appeal_pending: Unviesti una apellación
       your_appeal_rejected: Refugóse la to apellación
@@ -572,8 +578,8 @@ ast:
         one: "%{count} pallabra clave"
         other: "%{count} pallabres claves"
       statuses:
-        one: "%{count} artículu"
-        other: "%{count} artículos"
+        one: "%{count} publicación"
+        other: "%{count} publicaciones"
       title: Peñeres
     new:
       title: Amestar una peñera
@@ -637,7 +643,7 @@ ast:
     successful_sign_in_html: Anicióse correutamente la sesión col métodu «%{method}» dende %{ip} (%{browser})
   media_attachments:
     validations:
-      images_and_video: Nun se pue axuntar nengún videu a un artículu que yá contién imáxenes
+      images_and_video: Nun se pue axuntar nengún videu a una publicación que yá contién imáxenes
       too_many: Nun se puen axuntar más de 4 ficheros
   migrations:
     errors:
@@ -654,7 +660,7 @@ ast:
       sign_up:
         subject: "%{name} rexistróse"
     favourite:
-      subject: "%{name} marcó'l to artículu como favoritu"
+      subject: "%{name} marcó la to publicación como favorita"
     follow:
       body: "¡Agora %{name} siguete!"
       subject: "%{name} ta siguiéndote"
@@ -666,10 +672,10 @@ ast:
       subject: "%{name} mentóte"
       title: Mención nueva
     reblog:
-      body: "%{name} compartió'l to artículu:"
-      subject: "%{name} compartió'l to artículu"
+      body: "%{name} compartió la to publicación:"
+      subject: "%{name} compartió la to publicación"
     update:
-      subject: "%{name} editó un artículu"
+      subject: "%{name} editó una publicación"
   notifications:
     email_events_hint: 'Seleiciona los eventos de los que quies recibir avisos:'
   number:
@@ -798,12 +804,12 @@ ast:
         other: "%{count} vídeos"
     default_language: La mesma que la de la interfaz
     errors:
-      in_reply_not_found: L'artículu al que tentes de responder paez que nun esiste.
+      in_reply_not_found: La publicación a la que tentes de responder paez que nun esiste.
     pin_errors:
       direct: Nun se puen fixar los artículos que son visibles namás pa los usuarios mentaos
       limit: Yá fixesti'l númberu máximu d'artículos
-      ownership: Nun se pue fixar l'artículu d'otru perfil
-      reblog: Nun se pue fixar un artículu compartíu
+      ownership: Nun se pue fixar la publicación d'otru perfil
+      reblog: Nun se pue fixar una publicación compartida
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: Mensaxe direutu
@@ -817,12 +823,12 @@ ast:
     keep_direct: Caltener los mensaxes direutos
     keep_direct_hint: Nun desanicia nengún mensaxe direutu
     keep_media: Caltener los artículos con elementos multimedia
-    keep_media_hint: Nun desanicia nengún artículu de to que contenta elementos multimedia
+    keep_media_hint: Nun desanicia nenguna publicación de to que contenta elementos multimedia
     keep_pinned: Caltener los artículos fixaos
     keep_polls: Caltener les encuestes
     keep_polls_hint: Nun desanicia nenguna encuesta de to
     keep_self_bookmark: Caltener los artículos que metieres en Marcadores
-    keep_self_bookmark_hint: Nun desanicia nengún artículu que metieres en Marcadores
+    keep_self_bookmark_hint: Nun desanicia nenguna publicación que metieres en Marcadores
     min_age:
       '1209600': 2 selmanes
       '15778476': 6 meses
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 432737f8c3..7954a7b539 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -302,6 +302,7 @@ bg:
       title: Одитен дневник
       unavailable_instance: "(неналично име на домейн)"
     announcements:
+      back: Обратно към оповестяванията
       destroyed_msg: Успешно изтрито оповестяване!
       edit:
         title: Редактиране на оповестянето
@@ -310,6 +311,9 @@ bg:
       new:
         create: Създаване на оповестяване
         title: Ново оповестяване
+      preview:
+        explanation_html: 'Е-писмо ще се изпрати до <strong>%{display_count} потребители</strong>. Следният текст ще се включи в е-писмо:'
+        title: Нагледно известието за оповестяване
       publish: Публикуване
       published_msg: Успешно публикувано оповестяване!
       scheduled_for: Насрочено за %{time}
@@ -468,6 +472,32 @@ bg:
       new:
         title: Внос на блокирания на домейни
       no_file: Няма избран файл
+    fasp:
+      debug:
+        callbacks:
+          created_at: Създадено на
+          delete: Изтриване
+          ip: IP адрес
+          request_body: Тяло на заявката
+          title: Дебъгване на обратните обаждания
+      providers:
+        active: Дейно
+        base_url: Базов URL адрес
+        callback: Обратно обаждане
+        delete: Изтриване
+        edit: Редактиране на доставчик
+        finish_registration: Край на регистрирането
+        name: Име
+        providers: Доставчици
+        public_key_fingerprint: Пръстов отпечатък на публичен ключ
+        registration_requested: Изисква се регистриране
+        registrations:
+          confirm: Потвърждаване
+          reject: Отхвърляне
+        save: Запазване
+        select_capabilities: Избор на възможности
+        sign_in: Вход
+        status: Състояние
     follow_recommendations:
       description_html: "<strong>Препоръките за следване помагат на новите потребители бързо да намерят ново съдържание</strong>. Когато един потребител не е създавал достатъчно връзки, за да формира свои собствени препоръки за следване, тези акаунти ще бъдат препоръчани. Акаунтите ще бъдат генерирани всеки ден на базата на най-голяма скорошна ангажираност и най-голям брой местни последователи за даден език."
       language: За език
@@ -932,6 +962,7 @@ bg:
         chance_to_review_html: "<strong>Пораждани условия на услугата няма да се публикуват самодейно.</strong> Ще имате възможност да видите предварително резултата. Попълнете необходимите подробности, за да продължите."
         explanation_html: Предоставеният шаблон на условията за услугата е само за осведомителни цели и не трябва да се тълкува като правен съвет, по който и да е въпрос. Посъветвайте се със собствения си правен адвокат относно ситуацията ви и конкретни правни въпроси, които имате.
         title: Настройка на условията за услугата
+      going_live_on_html: На живо, в сила от %{date}
       history: История
       live: На живо
       no_history: Още няма записани промени в условията на услугата.
@@ -1200,6 +1231,7 @@ bg:
     too_fast: Образецът подаден пребързо, опитайте пак.
     use_security_key: Употреба на ключ за сигурност
     user_agreement_html: Прочетох и се съгласявам с <a href="%{terms_of_service_path}" target="_blank">условията на услугата</a> и <a href="%{privacy_policy_path}" target="_blank">политиката за поверителност</a>
+    user_privacy_agreement_html: Прочетох и има съгласието ми за <a href="%{privacy_policy_path}" target="_blank">политиката за поверителност</a>
   author_attribution:
     example_title: Примерен текст
     hint_html: Пишете ли новинарски статии или блогове извън Mastodon? Управлявайте как ви приписват авторството, когато са споделени в Mastodon.
@@ -1894,6 +1926,8 @@ bg:
     recovery_instructions_html: Ако изгубите достъп до телефона си, може да използвате долните кодовете за възстановяване, за да достъп до акаунта си. <strong>Запазете тези кодове на сигурно място</strong>. Например, можете да ги отпечатате и да ги складирате заедно с други важни документи.
     webauthn: Ключове за сигурност
   user_mailer:
+    announcement_published:
+      description: 'Администраторите на %{domain} оповестяват:'
     appeal_approved:
       action: Настройки на акаунта
       explanation: Жалбата, която изпратихте на %{appeal_date}, срещу нарушението за вашия акаунт, направено на %{strike_date}, е приета. Вашият акаунт е отново с добра репутация.
@@ -1926,8 +1960,8 @@ bg:
     terms_of_service_changed:
       agreement: Продължавайки употребата на %{domain}, съгласявате се с тези условия. Ако не сте съгласни с осъвременените условия, то може по всяко време да прекратите съгласието си с %{domain}, изтривайки акаунта си.
       changelog: 'Накратко, ето какво значи това обновяване за вас:'
-      description: 'Получавате това е-писмо, защото правим някои промени по условията ни на услугата при %{domain}. Насърчаваме ви предварително да прегледате обновените условия изцяло тук:'
-      description_html: Получавате това е-писмо, защото правим някои промени по условията ни на услугата при %{domain}. Насърчаваме ви предварително да прегледате <a href="%{path}" target="_blank">обновените условия изцяло тук</a>.
+      description: 'Получвате това е-писмо, защото сме направили някои промени в условията ни за услугата при %{domain}. Тези новости ще влязат в сила на %{date}. Насърчаваме ви да разгледате изцяло обновените условия тук:'
+      description_html: Получавате това е-писмо, защото правим някои промени по условията ни за услугата при %{domain}. Тези новости ще влязат в сила на <strong>%{date}</strong>. Насърчаваме ви предварително да прегледате <a href="%{path}" target="_blank">изцяло обновените условия тук</a>.
       sign_off: Отборът на %{domain}
       subject: Новости в нашите условия за ползване
       subtitle: Условията на услугата на %{domain} се променят
diff --git a/config/locales/br.yml b/config/locales/br.yml
index fbe91fcbd7..e61a43643e 100644
--- a/config/locales/br.yml
+++ b/config/locales/br.yml
@@ -47,6 +47,7 @@ br:
       demote: Argilañ
       disable: Skornañ
       disabled: Skornet
+      display_name: Anv diskouezet
       domain: Domani
       edit: Kemmañ
       email: Postel
@@ -66,6 +67,7 @@ br:
       moderation:
         active: Oberiant
         all: Pep tra
+        disabled: Diweredekaet
         pending: War ober
         silenced: Bevennet
         suspended: Astalet
@@ -77,6 +79,8 @@ br:
       public: Publik
       reject: Nac'hañ
       remove_header: Dilemel an talbenn
+      resend_confirmation:
+        send: Adkas al liamm-kadarnaat
       reset: Adderaouekaat
       reset_password: Adderaouekaat ar ger-tremen
       resubscribe: Adkoumanantiñ
@@ -97,7 +101,14 @@ br:
       web: Web
     action_logs:
       action_types:
+        approve_user: Aprouiñ an implijer
+        change_role_user: Kemmañ perzh an implijer
+        confirm_user: Aprouiñ an implijer
+        create_ip_block: Krouiñ ur reolenn IP
+        create_user_role: Krouiñ ur perzh
+        destroy_ip_block: Dilemel ar reolenn IP
         destroy_status: Dilemel ar c'hannad
+        reset_password_user: Adderaouekaat ar ger-tremen
         update_status: Hizivaat ar c'hannad
       actions:
         destroy_status_html: Dilamet eo bet toud %{target} gant %{name}
@@ -138,12 +149,15 @@ br:
       export: Ezporzhiañ
       import: Enporzhiañ
     domain_blocks:
+      confirm_suspension:
+        cancel: Nullañ
       domain: Domani
       new:
         create: Sevel ur stanker
         severity:
           noop: Hini ebet
           suspend: Astalañ
+      public_comment: Evezhiadenn foran
     email_domain_blocks:
       add_new: Ouzhpenniñ unan nevez
       delete: Dilemel
@@ -155,6 +169,18 @@ br:
         create: Ouzhpenniñ un domani
     export_domain_allows:
       no_file: Restr ebet diuzet
+    fasp:
+      debug:
+        callbacks:
+          delete: Dilemel
+          ip: Chomlec'h IP
+      providers:
+        active: Oberiant
+        delete: Dilemel
+        name: Anv
+        registrations:
+          confirm: Kadarnaat
+        status: Statud
     follow_recommendations:
       status: Statud
       suppressed: Dilamet
@@ -169,6 +195,7 @@ br:
           suspend: Astalañ
         policy: Reolennoù
       dashboard:
+        instance_languages_dimension: Yezhoù pennañ
         instance_statuses_measure: toudoù stoket
       delivery:
         all: Pep tra
@@ -179,6 +206,9 @@ br:
         title: Habaskadur
       purge: Spurjañ
       title: Kevread
+      total_blocked_by_us: Stanket ganeomp
+      total_followed_by_them: Heuliet ganto
+      total_followed_by_us: Heuliet ganeomp
       total_storage: Restroù media stag
     invites:
       filter:
@@ -188,6 +218,7 @@ br:
         title: Sil
       title: Pedadennoù
     ip_blocks:
+      add_new: Krouiñ ur reolenn
       delete: Dilemel
       expires_in:
         '1209600': 2 sizhunvezh
@@ -195,6 +226,9 @@ br:
         '31556952': 1 bloavezh
         '86400': 1 devezh
         '94670856': 3 bloavezh
+      new:
+        title: Krouiñ ur reolenn IP nevez
+      title: Reolennoù IP
     relays:
       delete: Dilemel
       disable: Diweredekaat
@@ -215,6 +249,7 @@ br:
       are_you_sure: Ha sur oc'h?
       comment:
         none: Hini ebet
+      confirm: Kadarnaat
       delete_and_resolve: Dilemel ar c'hannadoù
       forwarded: Treuzkaset
       no_one_assigned: Den ebet
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index c602b36c8c..17efbcb4b6 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -309,6 +309,7 @@ ca:
       title: Registre d'auditoria
       unavailable_instance: "(nom de domini no disponible)"
     announcements:
+      back: Torna als anuncis
       destroyed_msg: L’anunci s’ha esborrat amb èxit!
       edit:
         title: Edita l'anunci
@@ -317,6 +318,9 @@ ca:
       new:
         create: Crea un anunci
         title: Nou anunci
+      preview:
+        explanation_html: 'S''enviarà el correu-e a <strong>%{display_count} usuaris</strong>. S''hi inclourà el text següent:'
+        title: Vista prèvia de la notificació d'anunci
       publish: Publica
       published_msg: L’anunci s’ha publicat amb èxit!
       scheduled_for: Programat per a %{time}
@@ -475,6 +479,26 @@ ca:
       new:
         title: Importa dominis bloquejats
       no_file: No s'ha seleccionat cap fitxer
+    fasp:
+      debug:
+        callbacks:
+          created_at: Data de creació
+          delete: Elimina
+          ip: Adreça IP
+          request_body: Solicita el cos
+      providers:
+        active: Actiu
+        base_url: URL base
+        callback: Torna la trucada
+        delete: Elimina
+        edit: Editeu el perfil
+        name: Nom
+        registrations:
+          confirm: Confirma
+          reject: Rebutja
+        save: Desa
+        sign_in: Inicieu la sessió
+        status: Estat
     follow_recommendations:
       description_html: "<strong>Seguir les recomanacions ajuda als nous usuaris a trobar ràpidament contingut interessant</strong>. Quan un usuari no ha interactuat prou amb altres per a formar a qui seguir personalment, aquests comptes li seran recomanats. Es recalculen diàriament a partir d'una barreja de comptes amb els compromisos recents més alts i el nombre més alt de seguidors locals per a un idioma determinat."
       language: Per idioma
@@ -939,6 +963,7 @@ ca:
         chance_to_review_html: "<strong>Les condicions de servei generades no es publicaran automàticament.</strong> Tindreu l'oportunitat de revisar-ne els resultats. Empleneu els detalls necessaris per a procedir."
         explanation_html: La plantilla de condicions de servei proveïda ho és només a títol informatiu i no s'ha d'interpretar com a consell jurídic en cap cas. Consulteu el vostre propi servei legal sobre la vostra situació i les qüestions legals específiques que tingueu.
         title: Configuració de les condicions de servei
+      going_live_on_html: En vigor; des de %{date}
       history: Historial
       live: En ús
       no_history: Encara no hi ha enregistrats canvis en les condicions de servei.
@@ -948,6 +973,9 @@ ca:
       preview:
         explanation_html: 'Aquest correu-e s''enviarà a <strong>%{display_count} usuaris</strong> que han signat abans de %{date}. S''hi inclourà aquest text:'
         send_preview: Envia una vista prèvia a %{email}
+        send_to_all:
+          one: Envia %{display_count} correu-e
+          other: Envia %{display_count} correus-e
         title: Vista prèvia de la notificació de les condicions de servei
       publish: Publica
       published_on_html: Publicada el %{date}
@@ -1411,6 +1439,22 @@ ca:
       merge_long: Mantenir els registres existents i afegir-ne de nous
       overwrite: Sobreescriu
       overwrite_long: Reemplaça els registres actuals amb els nous
+    overwrite_preambles:
+      blocking_html:
+        one: Esteu a punt de <strong>reemplaçar la vostra llista de blocats</strong> amb fins a <strong>%{count} compte</strong> des de <strong>%{filename}</strong>.
+        other: Esteu a punt de <strong>reemplaçar la vostra llista de blocats</strong> amb fins a <strong>%{count} comptes</strong> des de <strong>%{filename}</strong>.
+      bookmarks_html:
+        one: Esteu a punt de <strong>reemplaçar els vostres marcadors</strong> amb fins a <strong>%{count} publicació</strong> des de <strong>%{filename}</strong>.
+        other: Esteu a punt de <strong>reemplaçar els vostres marcadors</strong> amb fins a <strong>%{count} publicacions</strong> des de <strong>%{filename}</strong>.
+      domain_blocking_html:
+        one: Esteu a punt de <strong>reemplaçar la vostra llista de dominis blocats</strong> amb fins a <strong>%{count} domini</strong> des de <strong>%{filename}</strong>.
+        other: Esteu a punt de <strong>reemplaçar la vostra llista de dominis blocats</strong> amb fins a <strong>%{count} dominis</strong> des de <strong>%{filename}</strong>.
+      following_html:
+        one: Esteu a punt de <strong>seguir</strong> fins a <strong>%{count} compte</strong> des de <strong>%{filename}</strong> i <strong>deixar de seguir la resta</strong>.
+        other: Esteu a punt de <strong>seguir</strong> fins a <strong>%{count} comptes</strong> des de <strong>%{filename}</strong> i <strong>deixar de seguir la resta</strong>.
+      lists_html:
+        one: Esteu a punt de <strong>reemplaçar les vostres llistes</strong> amb contactes de <strong>%{filename}</strong>. S'afegirà <strong>%{count} compte</strong> a les noves llistes.
+        other: Esteu a punt de <strong>reemplaçar les vostres llistes</strong> amb contactes de <strong>%{filename}</strong>. S'afegiran fins a <strong>%{count} comptes</strong> a les noves llistes.
     preface: Pots importar algunes les dades que has exportat des d'un altre servidor, com ara el llistat de les persones que estàs seguint o bloquejant.
     recent_imports: Importacions recents
     states:
@@ -1863,6 +1907,10 @@ ca:
     recovery_instructions_html: Si mai perds l'accés al teu telèfon, pots fer servir un dels codis de recuperació a continuació per a recuperar l'accés al teu compte. <strong>Cal mantenir els codis de recuperació en lloc segur</strong>. Per exemple, imprimint-los i guardar-los amb altres documents importants.
     webauthn: Claus de seguretat
   user_mailer:
+    announcement_published:
+      description: 'L''administració de %{domain} fa un anunci:'
+      subject: Anunci de servei
+      title: Anunci de servei de %{domain}
     appeal_approved:
       action: Configuració del compte
       explanation: L'apel·lació a l'acció contra el teu compte del %{strike_date} que has enviat el %{appeal_date} ha estat aprovada. El teu compte torna a estar en bon estat.
@@ -1893,6 +1941,8 @@ ca:
       subject: S'ha accedit al teu compte des d'una adreça IP nova
       title: Un inici de sessió nou
     terms_of_service_changed:
+      agreement: En continuar fent servir %{domain} accepteu aquestes condicions. Si no esteu d'acord amb els nous termes, podeu acabar el vostre acord amb %{domain} tot esborrant el vostre compte.
+      changelog: 'En un cop d''ull, això és el que aquest canvi us implica:'
       sign_off: L'equip de %{domain}
       subject: Actualitzacions de les condicions de servei
       subtitle: Les condicions de servei de %{domain} canvien
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 3ca73a3a14..37cd0b3bf6 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -99,7 +99,7 @@ cs:
         active: Aktivní
         all: Vše
         disabled: Deaktivován
-        pending: Čekající
+        pending: Nevyřízeno
         silenced: Omezeno
         suspended: Pozastavené
         title: Moderování
@@ -315,6 +315,7 @@ cs:
       title: Protokol auditu
       unavailable_instance: "(doména není k dispozici)"
     announcements:
+      back: Zpět na oznámení
       destroyed_msg: Oznámení bylo úspěšně odstraněno!
       edit:
         title: Upravit oznámení
@@ -323,6 +324,10 @@ cs:
       new:
         create: Vytvořit oznámení
         title: Nové oznámení
+      preview:
+        disclaimer: Vzhledem k tomu, že se od nich uživatelé nemohou odhlásit, měla by být e-mailová upozornění omezena na důležitá oznámení, jako je narušení osobních údajů nebo oznámení o uzavření serveru.
+        explanation_html: 'E-mail bude odeslán <strong>%{display_count} uživatelům</strong>. Následující text bude zahrnut do onoho e-mailu:'
+        title: Náhled oznámení
       publish: Zveřejnit
       published_msg: Oznámení bylo úspěšně zveřejněno!
       scheduled_for: Naplánováno na %{time}
@@ -491,6 +496,36 @@ cs:
       new:
         title: Importovat blokované domény
       no_file: Nebyl vybrán žádný soubor
+    fasp:
+      debug:
+        callbacks:
+          created_at: Vytvořeno
+          delete: Odstranit
+          ip: IP adresa
+          request_body: Tělo požadavku
+          title: Ladit callbacky
+      providers:
+        active: Aktivní
+        base_url: Základní URL
+        callback: Callback
+        delete: Smazat
+        edit: Upravit poskytovatele
+        finish_registration: Dokončit registraci
+        name: Jméno
+        providers: Poskytovatelé
+        public_key_fingerprint: Otisk veřejného klíče
+        registration_requested: Registrace požádána
+        registrations:
+          confirm: Potvrdit
+          description: Obdrželi jste registraci od FASP. Odmítněte ji, pokud jste ji neiniciovali. Pokud jste ji iniciovali, pečlivě porovnejte jméno a otisk klíče před jejím potvrzením.
+          reject: Zamítnout
+          title: Potvrdit registraci FASP
+        save: Uložit
+        select_capabilities: Vybrat možnosti
+        sign_in: Přihlásit se
+        status: Stav
+        title: Fediverse Auxiliary Service Providers
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Doporučená sledování pomáhají novým uživatelům rychle najít zajímavý obsah</strong>. Pokud uživatel neinteragoval s ostatními natolik, aby mu byla vytvořena doporučená sledování na míru, jsou použity tyto účty. Jsou přepočítávány na denní bázi ze směsi účtů s největším nedávným zapojením a nejvyšším počtem místních sledovatelů pro daný jazyk."
       language: Pro jazyk
@@ -967,6 +1002,7 @@ cs:
         chance_to_review_html: "<strong>Vygenerované podmínky služby nebudou zveřejněny automaticky.</strong> Výsledky budete mít možnost zkontrolovat. Pro pokračování vyplňte potřebné podrobnosti."
         explanation_html: Poskytovaná šablona služeb je určena pouze pro informační účely a neměla by být vykládána jako právní poradenství v jakékoli věci. Prosíme, konzultujte Vaši situaci a konkrétní právní otázky s Vaším pravním zástupcem.
         title: Nastavení podmínek služby
+      going_live_on_html: Publikováno, platné od %{date}
       history: Historie
       live: Živě
       no_history: Zatím nejsou zaznamenány žádné změny podmínek služby.
@@ -1990,6 +2026,10 @@ cs:
     recovery_instructions_html: Ztratíte-li někdy přístup ke svému telefonu, můžete k získání přístupu k účtu použít jeden ze záložních kódů. <strong>Uchovejte tyto kódy v bezpečí</strong>. Můžete si je například vytisknout a uložit je mezi jiné důležité dokumenty.
     webauthn: Bezpečnostní klíče
   user_mailer:
+    announcement_published:
+      description: 'Správci %{domain} oznamují:'
+      subject: Oznámení služby
+      title: Oznámení služby %{domain}
     appeal_approved:
       action: Možnosti Účtu
       explanation: Odvolání proti prohřešku vašeho účtu ze dne %{strike_date}, které jste podali %{appeal_date}, bylo schváleno. Váš účet je opět v pořádku.
@@ -2022,8 +2062,8 @@ cs:
     terms_of_service_changed:
       agreement: Pokračováním v používání %{domain} souhlasíte s těmito podmínkami. Pokud nesouhlasíte s aktualizovanými podmínkami, můžete svůj souhlas s %{domain} kdykoliv ukončit odstraněním vašeho účtu.
       changelog: 'Ve zkratce, zde je to, co tato změna znamená pro vás:'
-      description: 'Tento e-mail jste obdrželi, protože na %{domain} provádíme určité změny našich smluvních podmínek. Doporučujeme vám zkontrolovat aktualizované podmínky v plném znění zde:'
-      description_html: Tento e-mail jste obdrželi, protože na %{domain} provádíme určité změny našich smluvních podmínek. Doporučujeme Vám zkontrolovat <a href="%{path}" target="_blank">aktualizované termíny v plném znění zde</a>.
+      description: 'Tento e-mail jste obdrželi, protože na %{domain} provádíme určité změny našich smluvních podmínek. Tyto aktualizace budou platné od %{date}. Doporučujeme vám zkontrolovat aktualizované podmínky v celém znění zde:'
+      description_html: Tento e-mail jste obdrželi, protože na %{domain} provádíme určité změny našich smluvních podmínek. Tyto aktualizace budou platné od <strong>%{date}</strong>. Doporučujeme vám zkontrolovat <a href="%{path}" target="_blank">aktualizované podmínky v celém znění zde</a>.
       sign_off: Tým %{domain}
       subject: Aktualizace našich podmínek služby
       subtitle: Podmínky služby %{domain} se mění
diff --git a/config/locales/cy.yml b/config/locales/cy.yml
index c2cf685503..edd89fedc6 100644
--- a/config/locales/cy.yml
+++ b/config/locales/cy.yml
@@ -321,6 +321,7 @@ cy:
       title: Cofnod archwilio
       unavailable_instance: "(nid yw enw'r parth ar gael)"
     announcements:
+      back: Nôl i'r cyhoeddiadau
       destroyed_msg: Cyhoeddiad wedi'i ddileu'n llwyddiannus!
       edit:
         title: Golygu cyhoeddiad
@@ -329,6 +330,9 @@ cy:
       new:
         create: Creu cyhoeddiad
         title: Cyhoeddiad newydd
+      preview:
+        explanation_html: 'Bydd yr e-bost yn cael ei anfon at <strong>%{display_count} defnyddiwr</strong> . Bydd y testun canlynol yn cael ei gynnwys yn yr e-bost:'
+        title: Hysbysiad rhagolwg cyhoeddiad
       publish: Cyhoeddi
       published_msg: Cyhoeddiad wedi'i gyhoeddi'n llwyddiannus!
       scheduled_for: Wedi'i amserlenni ar gyfer %{time}
@@ -507,6 +511,36 @@ cy:
       new:
         title: Mewnforio blociau parth
       no_file: Heb ddewis ffeil
+    fasp:
+      debug:
+        callbacks:
+          created_at: Crëwyd am
+          delete: Dileu
+          ip: Cyfeiriad IP
+          request_body: Gofyn am y corff
+          title: Adalwasau Dadfygio
+      providers:
+        active: Gweithredol
+        base_url: URL sylfaen
+        callback: Adalwad
+        delete: Dileu
+        edit: Golygu Darparwr
+        finish_registration: Gorffen cofrestru
+        name: Enw
+        providers: Darparwyr
+        public_key_fingerprint: Ôl bys allwedd cyhoeddus
+        registration_requested: Cais am gofrestru
+        registrations:
+          confirm: Cadarnhau
+          description: Rydych wedi derbyn cofrestriad gan FASP. Gwrthodwch hyn os nad chi ofynnodd amdano. Os taw chi gychwynnodd hyn, cymharwch yr enw ac allwedd yr ôl bys yn ofalus cyn cadarnhau'r cofrestriad.
+          reject: Gwrthod
+          title: Cadarnhau Cofrestriad FASP
+        save: Cadw
+        select_capabilities: Dewis Galluoedd
+        sign_in: Mewngofnodi
+        status: Statws
+        title: Fediverse Auxiliary Service Providers
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Mae dilyn yr argymhellion yn helpu i ddefnyddwyr newydd ddod o hyd i gynnwys diddorol yn gyflym</strong>. Pan nad yw defnyddiwr wedi rhyngweithio digon ag eraill i ffurfio argymhellion dilyn personol, argymhellir y cyfrifon hyn yn lle hynny. Cânt eu hailgyfrifo'n ddyddiol o gymysgedd o gyfrifon gyda'r ymgysylltiadau diweddar uchaf a'r cyfrif dilynwyr lleol uchaf ar gyfer iaith benodol."
       language: Ar gyfer iaith
@@ -995,6 +1029,7 @@ cy:
         chance_to_review_html: "<strong>Ni fydd y telerau gwasanaeth sy'n cael eu cynhyrchu'n cael eu cyhoeddi'n awtomatig.</strong> Bydd cyfle i chi adolygu'r canlyniadau. Cwblhewch y manylion angenrheidiol i symud ymlaen."
         explanation_html: Mae'r templed telerau gwasanaeth sy'n cael eu darparu at ddibenion gwybodaeth yn unig, ac ni ddylid ei ddehongli fel cyngor cyfreithiol ar unrhyw bwnc. Ymgynghorwch â'ch cyngor cyfreithiol eich hun ar eich sefyllfa ag unrhyw gwestiynau cyfreithiol penodol sydd gennych.
         title: Telerau Gosod Gwasanaeth
+      going_live_on_html: Byw, effeithiol %{date}
       history: Hanes
       live: Byw
       no_history: Nid oes unrhyw newidiadau i delerau gwasanaeth wedi'u cofnodi eto.
@@ -1285,6 +1320,7 @@ cy:
     too_fast: Cafodd y ffurflen ei chyflwyno'n rhy gyflym, ceisiwch eto.
     use_security_key: Defnyddiwch allwedd diogelwch
     user_agreement_html: Rwyf wedi darllen ac yn cytuno i <a href="%{terms_of_service_path}" target="_blank">delerau gwasanaeth</a> a'r <a href="%{privacy_policy_path}" target="_blank">polisi preifatrwydd</a>
+    user_privacy_agreement_html: Rwyf wedi darllen ac yn cytuno i'r <a href="%{privacy_policy_path}" target="_blank">polisi preifatrwydd</a>
   author_attribution:
     example_title: Testun enghreifftiol
     hint_html: Ydych chi'n ysgrifennu erthyglau newyddion neu flog y tu allan i Mastodon? Rheolwch sut y byddwch yn cael eich cydnabod pan fyddan nhw'n cael eu rhannu ar Mastodon.
@@ -1411,7 +1447,7 @@ cy:
   filters:
     contexts:
       account: Proffilau
-      home: Cartref a rhestrau
+      home: Ffrwd gartref
       notifications: Hysbysiadau
       public: Ffrydiau cyhoeddus
       thread: Sgyrsiau
@@ -2075,6 +2111,10 @@ cy:
     recovery_instructions_html: Os ydych chi'n colli mynediad i'ch ffôn, mae modd i chi ddefnyddio un o'r codau adfer isod i gael mynediad at eich cyfrif. <strong>Cadwch y codau adfer yn breifat</strong>. Er enghraifft, gallwch chi eu argraffu a'u cadw gyda dogfennau eraill pwysig.
     webauthn: Allweddi diogelwch
   user_mailer:
+    announcement_published:
+      description: 'Mae gweinyddwyr %{domain} yn gwneud cyhoeddiad:'
+      subject: Cyhoeddiad gwasanaeth
+      title: Cyhoeddiad gwasanaeth %{domain}
     appeal_approved:
       action: Gosodiadau Cyfrif
       explanation: Mae apêl y rhybudd yn erbyn eich cyfrif ar %{strike_date} a gyflwynwyd gennych ar %{appeal_date} wedi'i chymeradwyo. Mae eich cyfrif unwaith eto yn gadarnhaol.
@@ -2107,8 +2147,8 @@ cy:
     terms_of_service_changed:
       agreement: Drwy barhau i ddefnyddio %{domain}, rydych yn cytuno i'r telerau hyn. Os ydych yn anghytuno â'r telerau a ddiweddarwyd, gallwch derfynu eich cytundeb â %{domain} ar unrhyw adeg drwy ddileu eich cyfrif.
       changelog: 'Yn fyr, dyma beth mae''r diweddariad hwn yn ei olygu i chi:'
-      description: 'Rydych yn derbyn yr e-bost hwn oherwydd ein bod yn gwneud rhai newidiadau i''n telerau gwasanaeth yn %{domain}. Rydym yn eich annog i adolygu''r telerau diweddaraf yn llawn yma:'
-      description_html: Rydych yn derbyn yr e-bost hwn oherwydd ein bod yn gwneud rhai newidiadau i'n telerau gwasanaeth yn %{domain}. Rydym yn eich annog i adolygu'r <a href="%{path}" target="_blank">telerau diweddaraf yn llawn yma</a> .
+      description: 'Rydych yn derbyn yr e-bost hwn oherwydd ein bod yn gwneud rhai newidiadau i''n telerau gwasanaeth yn %{domain}. Bydd y diweddariadau hyn yn dod i rym ar %{date}. Rydym yn eich annog i adolygu''r telerau diweddaraf yn llawn yma:'
+      description_html: Rydych yn derbyn yr e-bost hwn oherwydd ein bod yn gwneud rhai newidiadau i'n telerau gwasanaeth yn %{domain}. Bydd y diweddariadau hyn yn dod i rym ar <strong>%{date}</strong> . Rydym yn eich annog i adolygu'r <a href="%{path}" target="_blank">telerau diweddaraf yn llawn yma</a> .
       sign_off: Tîm %{domain}
       subject: Diweddariadau i'n telerau gwasanaeth
       subtitle: Mae telerau gwasanaeth %{domain} yn newid
diff --git a/config/locales/da.yml b/config/locales/da.yml
index b567fa016c..63a414811e 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -9,12 +9,12 @@ da:
   accounts:
     followers:
       one: Følger
-      other: tilhængere
+      other: Følgere
     following: Følger
     instance_actor_flash: Denne konto er en virtuel aktør repræsenterende selve serveren og ikke en individuel bruger. Den anvendes til fællesformål og bør ikke suspenderes.
     last_active: senest aktiv
     link_verified_on: Ejerskab af dette link blev tjekket %{date}
-    nothing_here: Der er intet hér!
+    nothing_here: Der er intet her!
     pin_errors:
       following: Man skal allerede følge den person, man ønsker at støtte
     posts:
@@ -108,11 +108,11 @@ da:
       not_subscribed: Abonnerer ikke
       pending: Afventende vurdering
       perform_full_suspension: Suspendér
-      previous_strikes: Tidligere anmeldelser (strikes)
+      previous_strikes: Tidligere anmeldelser
       previous_strikes_description_html:
         one: Denne konto har <strong>en</strong> anmeldelse.
         other: Denne konto har <strong>%{count}</strong> anmeldelser.
-      promote: Fremhæv
+      promote: Forfrem
       protocol: Protokol
       public: Offentlig
       push_subscription_expires: PuSH-abonnement udløber
@@ -123,9 +123,9 @@ da:
       remote_suspension_irreversible: Denne kontos data er slettet permanent.
       remote_suspension_reversible_hint_html: Kontoen er suspenderet på den pågældende server, og kontodata fjernes fuldstændig pr. %{date}. Indtil da vil fjernserveren kunne foretage en komplet reetablering af kontoen. Ønskes alle kontodata fjernet straks, kan dette gøres nedenfor.
       remove_avatar: Fjern profilbillede
-      remove_header: Fjern overskrift
+      remove_header: Fjern banner
       removed_avatar_msg: "%{username}s profilbillede fjernet"
-      removed_header_msg: "%{username}s overskriftsbillede fjernet"
+      removed_header_msg: "%{username}s banner fjernet"
       resend_confirmation:
         already_confirmed: Denne bruger er allerede bekræftet
         send: Gensend bekræftelseslink
@@ -141,8 +141,8 @@ da:
       security_measures:
         only_password: Kun adgangskode
         password_and_2fa: Adgangskode og 2FA
-      sensitive: Gennemtving sensitiv
-      sensitized: Markeret som sensitiv
+      sensitive: Gennemtving følsom
+      sensitized: Markeret som følsom
       shared_inbox_url: Delt indbakke-URL
       show:
         created_reports: Indsendte anmeldelser
@@ -160,7 +160,7 @@ da:
       unblock_email: Afblokér e-mailadresse
       unblocked_email_msg: "%{username}s e-mail-adresse afblokeret"
       unconfirmed_email: Ubekræftet e-mail
-      undo_sensitized: Fortryd gennemtving sensitiv
+      undo_sensitized: Fortryd gennemtving-følsom
       undo_silenced: Fortryd begrænsning
       undo_suspension: Fortryd suspendering
       unsilenced_msg: "%{username}s kontobegrænsning er fjernet"
@@ -169,8 +169,8 @@ da:
       username: Brugernavn
       view_domain: Vis domæneoversigt
       warn: Advar
-      web: Web
-      whitelisted: Tilladt for federering
+      web: Net
+      whitelisted: Tilladt for føderation
     action_logs:
       action_types:
         approve_appeal: Godkend appel
@@ -225,8 +225,8 @@ da:
         sensitive_account: Gennemtving sensitiv konto
         silence_account: Begræns konto
         suspend_account: Suspendér konto
-        unassigned_report: Fjer anmeldelsestildeling
-        unblock_email_account: Afblokér e-mailadresse
+        unassigned_report: Fjern anmeldelsestildeling
+        unblock_email_account: Fjern blokering af e-mailadresse
         unsensitive_account: Fjern Gennemtving sensitiv konto
         unsilence_account: Fjern kontobegrænselse
         unsuspend_account: Afsuspendér konto
@@ -309,6 +309,7 @@ da:
       title: Revisionslog
       unavailable_instance: "(domænenavn utilgængeligt)"
     announcements:
+      back: Retur til annonceringer
       destroyed_msg: Bekendtgørelsen er slettet!
       edit:
         title: Redigér bekendtgørelse
@@ -317,6 +318,10 @@ da:
       new:
         create: Opret bekendtgørelse
         title: Ny bekendtgørelse
+      preview:
+        disclaimer: Da brugere ikke kan fravælge e-mailnotifikationer, bør disse begrænses til vigtige emner som f.eks. personlige databrud eller serverlukninger.
+        explanation_html: 'E-mailen sendes til <strong>%{display_count} brugere</strong>. Flg. tekst medtages i e-mailen:'
+        title: Forhåndsvis annonceringsnotifikation
       publish: Publicér
       published_msg: Bekendtgørelsen er publiceret!
       scheduled_for: Planlagt til %{time}
@@ -374,7 +379,7 @@ da:
         other: "<strong>%{count}</strong> verserende anmeldelser"
       pending_tags_html:
         one: "<strong>%{count}</strong> afventende hashtag"
-        other: "<strong>%{count}</strong> afventende hashtags"
+        other: "<strong>%{count}</strong> afventende etiketter"
       pending_users_html:
         one: "<strong>%{count}</strong> afventende bruger"
         other: "<strong>%{count}</strong> afventende brugere"
@@ -475,6 +480,36 @@ da:
       new:
         title: Import af domæneblokeringer
       no_file: Ingen fill udvalgt
+    fasp:
+      debug:
+        callbacks:
+          created_at: Oprettet d.
+          delete: Slet
+          ip: IP-adresse
+          request_body: Anmodningsbrødtekst
+          title: Fejlfindingstilbagekald
+      providers:
+        active: Aktiv
+        base_url: Basis-URL
+        callback: Tilbagekald
+        delete: Slet
+        edit: Redigér udbyder
+        finish_registration: Færdiggør registrering
+        name: Navn
+        providers: Udbydere
+        public_key_fingerprint: Offentlig nøgle-fingeraftryk
+        registration_requested: Registrering kræves
+        registrations:
+          confirm: Bekræft
+          description: Er en registrering er modtaget fra en FASP, så afvis den, hvis man ikke startede pocessen. Startede man processen, tjek omhyggeligt navn og nøglefingeraftryk, før registreringen bekræftes.
+          reject: Afvis
+          title: Bekræft FASP-registrering
+        save: Gem
+        select_capabilities: Vælg kapaciteter
+        sign_in: Log ind
+        status: Status
+        title: Fediverse Auxiliary Service Providers
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Følg-anbefalinger hjælpe nye brugere til hurtigt at finde interessant indhold</strong>. Når en bruger ikke har interageret nok med andre til at generere personlige følg-anbefalinger, anbefales disse konti i stedet. De revurderes dagligt baseret på en blanding af konti med de flest nylige engagementer og fleste lokale følger-antal for et givet sprog."
       language: For sprog
@@ -735,7 +770,7 @@ da:
         manage_settings: Håndtere indstillinger
         manage_settings_description: Tillader brugere at ændre webstedsindstillinger
         manage_taxonomies: Håndtere taksonomier
-        manage_taxonomies_description: Tillader brugere at gennemse tenderende indhold og opdatere hashtag-indstillinger
+        manage_taxonomies_description: Tillader brugere at gennemse tenderende indhold og opdatere etiket-indstillinger
         manage_user_access: Håndtere brugeradgang
         manage_user_access_description: Tillader brugere at deaktivere andre brugeres tofaktorgodkendelse, skifte deres e-mailadresse og nulstille deres adgangskode
         manage_users: Håndtere brugere
@@ -925,7 +960,7 @@ da:
       reset: Nulstil
       review: Gennmgangsstatus
       search: Søg
-      title: Hashtags
+      title: Etiketter
       updated_msg: Hashtag-indstillinger opdateret
     terms_of_service:
       back: Tilbage til Tjenestevilkår
@@ -939,6 +974,7 @@ da:
         chance_to_review_html: "<strong>De genererede Tjenestevilkår offentliggøres ikke automatisk.</strong> Man vil have mulighed for at gennemgå resultaterne. Udfyld venligst de nødvendige oplysninger for at fortsætte."
         explanation_html: Tjenestevilkår-skabelonen er alene til orientering og bør ikke fortolkes som juridisk rådgivning om noget emne. Kontakt en juridiske rådgiver vedr. den aktuelle situation samt specifikke juridiske spørgsmål, man måtte have.
         title: Opsætning af Tjenestevilkår
+      going_live_on_html: Ikrafttræden pr. %{date}
       history: Historik
       live: Live
       no_history: Der er endnu ingen registrerede ændringer af vilkårene for tjenesten.
@@ -1014,14 +1050,14 @@ da:
           tag_servers_dimension: Topservere
           tag_servers_measure: forskellige servere
           tag_uses_measure: anvendelser i alt
-        description_html: Disse er hashtags, som pt. vises i en masse indlæg, som serveren ser. Det kan hjælpe brugerne til at finde ud af, hvad folk taler mest om pt. Ingen hashtags vises offentligt, før man godkender dem.
+        description_html: Disse er etiketter, som pt. vises i en masse indlæg, som serveren ser. Det kan hjælpe brugerne til at finde ud af, hvad folk taler mest om pt. Ingen etiketter vises offentligt, før man godkender dem.
         listable: Kan foreslås
         no_tag_selected: Intet tag ændret (da intet var valgt)
         not_listable: Foreslås ikke
         not_trendable: Vises ikke under tendenser
         not_usable: Kan ikke anvendes
         peaked_on_and_decaying: Toppede pr. %{date}, nu for nedadgående
-        title: Populære hashtags
+        title: Populære etiketter
         trendable: Kan vises under tendenser
         trending_rank: 'Populær #%{rank}'
         usable: Kan anvendes
@@ -1093,7 +1129,7 @@ da:
       new_trending_statuses:
         title: Populære opslag
       new_trending_tags:
-        title: Populære hashtags
+        title: Populære etiketter
       subject: Nye tendenser klar til gennemgang på %{instance}
   aliases:
     add_new: Opret alias
@@ -1104,7 +1140,7 @@ da:
     remove: Fjern aliaslinkning
   appearance:
     advanced_web_interface: Avanceret webgrænseflade
-    advanced_web_interface_hint: 'Ønsker du udnytte hele skærmbredden, lader den avancerede webgrænseflade dig opsætte mange forskellige kolonner for at se så meget information på samme tid som ønsket: Hjem, notifikationer, federeret tidslinje, et hvilket som helst antal lister og hashtags.'
+    advanced_web_interface_hint: 'Ønsker du udnytte hele skærmbredden, lader den avancerede netgrænseflade dig opsætte mange forskellige kolonner for at se så meget information på samme tid som ønsket: Hjem, notifikationer, fødereret tidslinje, et hvilket som helst antal lister og etiketter.'
     animations_and_accessibility: Animationer og tilgængelighed
     confirmation_dialogs: Bekræftelsesdialoger
     discovery: Opdagelse
@@ -1326,13 +1362,13 @@ da:
     csv: CSV
     domain_blocks: Domæneblokeringer
     lists: Lister
-    mutes: Du tavsgør
+    mutes: Du skjuler
     storage: Medielagerplads
   featured_tags:
     add_new: Tilføj nyt
     errors:
-      limit: Det maksimale antal hashtags er allerede fremhævet
-    hint_html: "<strong>Hvad er fremhævede hashtags?</strong> De vises i en fremtrædende position på din offentlige profil og giver folk mulighed for at gennemse dine offentlige indlæg specifikt under disse hashtags. De er et fantastisk værktøj til at holde styr på kreative værker eller langsigtede projekter."
+      limit: Det maksimale antal etiketter er allerede fremhævet
+    hint_html: "<strong>Hvad er fremhævede etiketter?</strong> De vises i en fremtrædende position på din offentlige profil og giver folk mulighed for at gennemse dine offentlige indlæg specifikt under disse etiketter. De er et fantastisk værktøj til at holde styr på kreative værker eller langsigtede projekter."
   filters:
     contexts:
       account: Profiler
@@ -1432,7 +1468,7 @@ da:
         other: Man er ved at <strong>erstatte sine lister</strong> med indhold fra <strong>%{filename}</strong>. Op til <strong>%{count} konti</strong> føjes til nye lister.
       muting_html:
         one: Man er ved at <strong>sin liste over en tavsgjort konto</strong> med <strong>%{count} konto</strong> fra <strong>%{filename}</strong>.
-        other: Man er ved at <strong>sin liste over tavsgjorte konti</strong> med op til <strong>%{count} konti</strong> fra <strong>%{filename}</strong>.
+        other: Du er ved at <strong>erstatte din liste over skjulte kontoer</strong> med op til <strong>%{count} kontoer</strong> fra <strong>%{filename}</strong>.
     preambles:
       blocking_html:
         one: Man er ved at <strong>blokere</strong> <strong>%{count} konto</strong> fra <strong>%{filename}</strong>.
@@ -1451,7 +1487,7 @@ da:
         other: Man er ved at tilføje <strong>%{count} konti</strong> fra <strong>%{filename}</strong> til sine <strong>lister</strong>. Nye lister oprettes, hvis der ikke findes nogen liste at tilføje til.
       muting_html:
         one: Man er ved at <strong>tavsgøre</strong> <strong>%{count} konto</strong> fra <strong>%{filename}</strong>.
-        other: Man er ved at <strong>tavsgøre</strong> op til <strong>%{count} konto</strong> fra <strong>%{filename}</strong>.
+        other: Du er ved at <strong>skjule</strong> op til <strong>%{count} kontoer</strong> fra <strong>%{filename}</strong>.
     preface: Du kan importere data, du har eksporteret fra en anden server, såsom en liste over folk du følger eller blokerer.
     recent_imports: Seneste importer
     states:
@@ -1468,11 +1504,11 @@ da:
       domain_blocking: Importerer blokerede konti
       following: Importerer fulgte konti
       lists: Import af lister
-      muting: Importerer tavsgjorte konti
+      muting: Importerer skjulte kontoer
     type: Importtype
     type_groups:
       constructive: Følger og Bogmærker
-      destructive: Blokeringer og tavsgjorte
+      destructive: Blokerede og skjulte kontoer
     types:
       blocking: Blokeringsliste
       bookmarks: Bogmærker
@@ -1529,7 +1565,7 @@ da:
           follow: e-mailnotifikationer om nye følgere
           follow_request: e-mailnotifikationer om følgeanmodninger
           mention: e-mailnotifikationer om omtaler
-          reblog: e-mailnotifikationer om boosts
+          reblog: e-mailnotifikationer om fremhævelser
       resubscribe_html: Har man afmeldt sig ved en fejl, kan man gentilmelde sig via <a href="%{settings_path}">indstillingerne E-mailnotifikationer</a>.
       success_html: Man vil ikke længere modtage %{type} for Mastodon på %{domain} til e-mailen %{email}.
       title: Opsig abonnement
@@ -1574,7 +1610,7 @@ da:
     title: Moderation
   move_handler:
     carry_blocks_over_text: Denne bruger er flyttet fra %{acct}, som du har haft blokeret.
-    carry_mutes_over_text: Denne bruger er flyttet fra %{acct}, som du har haft tavsgjort.
+    carry_mutes_over_text: Denne bruger er flyttet fra %{acct}, som du har haft skjult.
     copy_account_note_text: 'Denne bruger er flyttet fra %{acct}, hvor dine tidligere noter om dem var:'
   navigation:
     toggle_menu: Åbn/luk menu
@@ -1605,9 +1641,9 @@ da:
     poll:
       subject: En afstemning fra %{name} er afsluttet
     reblog:
-      body: 'Dit indlæg blev boostet af %{name}:'
-      subject: "%{name} boostede dit indlæg"
-      title: Nyt boost
+      body: 'Dit indlæg blev fremhævet af %{name}:'
+      subject: "%{name} fremhævede dit indlæg"
+      title: Ny fremhævelse
     status:
       subject: "%{name} har netop postet"
     update:
@@ -1704,7 +1740,7 @@ da:
     content_warning: 'Indholdsadvarsel:'
     descriptions:
       account: Offentlige indlæg fra @%{acct}
-      tag: 'Offentlige indlæg tagget #%{hashtag}'
+      tag: 'Offentlige indlæg etiketteret #%{hashtag}'
   scheduled_statuses:
     over_daily_limit: Den daglige grænse på %{limit} planlagte indlæg er nået
     over_total_limit: Grænsen på %{limit} planlagte indlæg er nået
@@ -1769,7 +1805,7 @@ da:
     development: Udvikling
     edit_profile: Redigér profil
     export: Eksport
-    featured_tags: Udvalgte hashtags
+    featured_tags: Udvalgte etiketter
     import: Import
     import_and_export: Import og eksport
     migrate: Kontomigrering
@@ -1805,12 +1841,12 @@ da:
       video:
         one: "%{count} video"
         other: "%{count} videoer"
-    boosted_from_html: Boostet fra %{acct_link}
+    boosted_from_html: Fremhævet fra %{acct_link}
     content_warning: 'Indholdsadvarsel: %{warning}'
     default_language: Samme som UI-sproget
     disallowed_hashtags:
-      one: 'indeholdte et ikke tilladt hashtag: %{tags}'
-      other: 'indeholdte de ikke tilladte hashtags: %{tags}'
+      one: 'indeholdte en ikke tilladt etiket: %{tags}'
+      other: 'indeholdte de ikke tilladte etiketter: %{tags}'
     edited_at_html: Redigeret %{date}
     errors:
       in_reply_not_found: Indlægget, der forsøges besvaret, ser ikke ud til at eksistere.
@@ -1819,7 +1855,7 @@ da:
       direct: Indlæg, som kun kan ses af omtalte brugere, kan ikke fastgøres
       limit: Maksimalt antal indlæg allerede fastgjort
       ownership: Andres indlæg kan ikke fastgøres
-      reblog: Et boost kan ikke fastgøres
+      reblog: En fremhævelse kan ikke fastgøres
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direkte
@@ -1835,9 +1871,9 @@ da:
     exceptions: Undtagelser
     explanation: Sletning af indlæg er en ressourcekrævende operation, hvorfor dette sker gradvist over tid, når serveren ellers ikke er optaget. Indlæg kan derfor blive slettet efter, at de reelt har passeret aldersgrænsen.
     ignore_favs: Ignorér favoritter
-    ignore_reblogs: Ignorér boosts
+    ignore_reblogs: Ignorér fremhævelser
     interaction_exceptions: Undtagelser baseret på interaktioner
-    interaction_exceptions_explanation: Bemærk, at det ikke garanteres, at indlæg slettes, hvis de når under favorit- eller boost-tærsklerne efter én gang at været nået over dem.
+    interaction_exceptions_explanation: Bemærk, at det ikke garanteres, at indlæg slettes, hvis de når under favorit- eller fremhævelses-tærsklerne efter én gang at været nået over dem.
     keep_direct: Behold direkte besked
     keep_direct_hint: Sletter ingen af dine direkte beskeder
     keep_media: Behold indlæg med medievedhæftninger
@@ -1862,8 +1898,8 @@ da:
     min_age_label: Alderstærskel
     min_favs: Behold indlæg favoritmarkeret mindst
     min_favs_hint: Sletter ingen egne indlæg, som har modtaget minimum dette antal favoritmarkeringer. Lad stå tomt for at slette indlæg uanset favoritmarkeringer
-    min_reblogs: Behold indlæg boostet mindst
-    min_reblogs_hint: Sletter ingen egne indlæg, som er boostet flere end dette antal gange. Lad stå tomt for at ignorere denne tærskel under sletning
+    min_reblogs: Behold indlæg fremhævet mindst
+    min_reblogs_hint: Sletter ingen af egne indlæg, som er fremhævet flere end dette antal gange. Lad stå tomt for at ignorere denne tærskel under sletning
   stream_entries:
     sensitive_content: Sensitivt indhold
   strikes:
@@ -1904,6 +1940,10 @@ da:
     recovery_instructions_html: Mister du nogensinde adgang til din mobil, kan en af gendannelseskoderne nedenfor bruges til at opnå adgang til din konto. <strong>Opbevar disse et sikkert sted</strong>. De kan f.eks. udskrives og gemmes sammen med andre vigtige dokumenter.
     webauthn: Sikkerhedsnøgler
   user_mailer:
+    announcement_published:
+      description: 'Administratorerne på %{domain} udsender en annoncering:'
+      subject: Tjenesteannoncering
+      title: "%{domain}-tjenesteannoncering"
     appeal_approved:
       action: Kontoindstillinger
       explanation: Appellen af kontoadvarslen fra %{strike_date}, indsendt af dig pr. %{appeal_date}, er blevet godkendt. Din kontostatus er igen god.
@@ -1936,8 +1976,8 @@ da:
     terms_of_service_changed:
       agreement: Ved at fortsætte med at bruge %{domain}, accepteres disse vilkår. Kan du ikke acceptere de opdaterede vilkår, kan din aftale med %{domain} til enhver tid opsiges ved at slette din konto.
       changelog: 'Med ét blik er her, hvad denne opdatering rent praktisk betyder:'
-      description: 'Man modtager denne e-mail, fordi der foretages nogle ændringer i vores Tjenestevilkår på %{domain}. Man opfordres til at gennemgå de opdaterede vilkår fuldt ud her:'
-      description_html: Man modtager denne e-mail, fordi der foretages nogle ændringer i vores Tjenestevilkår på %{domain}. Man opfordres til at gennemgå de <a href="%{path}" target="_blank">opdaterede vilkår fuldt ud her</a>.
+      description: 'Man modtager denne e-mail, idet vi foretager nogle ændringer i vores Tjenestevilkår på %{domain}. Disse opdateringer træder i kraft pr. %{date}. Man opfordres til at gennemgå de opdaterede vilkår til fulde her:'
+      description_html: Man modtager denne e-mail, idet vi foretager nogle ændringer i vores Tjenestevilkår på %{domain}. Disse opdateringer træder i kraft pr. <strong>%{date}</strong>. Man opfordres til at gennemgå de <a href="%{path}" target="_blank">opdaterede vilkår til fulde her</a>.
       sign_off: "%{domain}-teamet"
       subject: Opdatering af Tjenestevilkår
       subtitle: Tjenestevilkår for %{domain} ændres
@@ -2003,8 +2043,8 @@ da:
         one: "%{people} person de seneste 2 dage"
         other: "%{people} personer de seneste 2 dage"
       hashtags_subtitle: Udforsk de seneste 2 dages tendenser
-      hashtags_title: Populære hashtags
-      hashtags_view_more: Se flere populære hashtags
+      hashtags_title: Populære etiketter
+      hashtags_view_more: Se flere populære etiketter
       post_action: Skriv
       post_step: Sig hej til verden med tekst, fotos, videoer eller afstemninger.
       post_title: Opret det første indlæg
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 17c5098e20..0842ab73d7 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -309,6 +309,7 @@ de:
       title: Audit-Log
       unavailable_instance: "(Server nicht verfügbar)"
     announcements:
+      back: Zurück zu den Ankündigungen
       destroyed_msg: Ankündigung erfolgreich gelöscht!
       edit:
         title: Ankündigung bearbeiten
@@ -317,6 +318,10 @@ de:
       new:
         create: Ankündigung erstellen
         title: Neue Ankündigung
+      preview:
+        disclaimer: Da sich Profile nicht davon abmelden können, sollten Benachrichtigungen per E-Mail auf wichtige Ankündigungen wie z. B. zu Datenpannen oder Serverabschaltung beschränkt sein.
+        explanation_html: 'Die E-Mail wird an <strong>%{display_count} Nutzer*innen</strong> gesendet. Folgendes wird in der E-Mail enthalten sein:'
+        title: Vorschau der Ankündigung
       publish: Veröffentlichen
       published_msg: Ankündigung erfolgreich veröffentlicht!
       scheduled_for: Geplant für %{time}
@@ -475,6 +480,33 @@ de:
       new:
         title: Domains importieren
       no_file: Keine Datei ausgewählt
+    fasp:
+      debug:
+        callbacks:
+          created_at: Erstellt am
+          delete: Entfernen
+          ip: IP-Adresse
+      providers:
+        active: Aktiv
+        base_url: Basis-URL
+        delete: Entfernen
+        edit: Anbieter bearbeiten
+        finish_registration: Registrierung abschließen
+        name: Name
+        providers: Anbieter
+        public_key_fingerprint: Fingerabdruck des öffentlichen Schlüssels
+        registration_requested: Registrierung angefordert
+        registrations:
+          confirm: Bestätigen
+          description: Sie haben eine Registrierung von einer FASP erhalten. Lehnen Sie ab, wenn Sie dies nicht initiiert haben. Wenn Sie dies initiiert haben, vergleichen Sie Namen und Fingerabdruck vor der Bestätigung der Registrierung.
+          reject: Ablehnen
+          title: FASP-Registrierung bestätigen
+        save: Speichern
+        select_capabilities: Leistungsfähigkeit auswählen
+        sign_in: Anmelden
+        status: Status
+        title: Fediverse Auxiliary Service Providers
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Follower-Empfehlungen helfen neuen Nutzer*innen, interessante Inhalte schnell zu finden</strong>. Wenn ein*e Nutzer*in noch nicht genug mit anderen interagiert hat, um personalisierte Follower-Empfehlungen zu erhalten, werden stattdessen diese Profile vorgeschlagen. Sie werden täglich, basierend auf einer Mischung aus am meisten interagierenden Konten und jenen mit den meisten Followern für eine bestimmte Sprache neu berechnet."
       language: Für Sprache
@@ -719,7 +751,7 @@ de:
         manage_appeals: Einsprüche verwalten
         manage_appeals_description: Erlaubt es Benutzer*innen, Entscheidungen der Moderator*innen zu widersprechen
         manage_blocks: Sperrungen verwalten
-        manage_blocks_description: Erlaubt Nutzer*innen das Sperren von E-Mail-Providern und IP-Adressen
+        manage_blocks_description: Erlaubt Nutzer*innen das Sperren von E-Mail-Anbietern und IP-Adressen
         manage_custom_emojis: Eigene Emojis verwalten
         manage_custom_emojis_description: Erlaubt es Benutzer*innen, eigene Emojis auf dem Server zu verwalten
         manage_federation: Föderation verwalten
@@ -939,6 +971,7 @@ de:
         chance_to_review_html: "<strong>Die durch diese Vorlage erstellten Nutzungsbedingungen werden nicht automatisch veröffentlicht.</strong> Du wirst alles noch einmal überprüfen können. Bitte fülle alle Felder mit den notwendigen Informationen aus, um fortzufahren."
         explanation_html: Die Vorlage für die Nutzungsbedingungen dient ausschließlich zu Informationszwecken und sollte nicht als Rechtsberatung zu einem bestimmten Thema verstanden werden. Bei rechtlichen Fragen, konsultiere bitte deinen Rechtsbeistand.
         title: Nutzungsbedingungen festlegen
+      going_live_on_html: Aktuell gültig (ab %{date})
       history: Verlauf
       live: Aktuell gültig
       no_history: Es wurden noch keine Änderungen an den Nutzungsbedingungen aufgezeichnet.
@@ -946,7 +979,7 @@ de:
       notified_on_html: Nutzer*innen wurden am %{date} benachrichtigt
       notify_users: Nutzer*innen benachrichtigen
       preview:
-        explanation_html: 'Diese E-Mail wird an <strong>%{display_count} Nutzer*innen</strong> gesendet, die sich vor dem %{date} registriert haben. Der nachfolgende Text wird in der E-Mail enthalten sein:'
+        explanation_html: 'Die E-Mail wird an <strong>%{display_count} Nutzer*innen</strong> gesendet, die sich vor dem %{date} registriert haben. Folgendes wird in der E-Mail enthalten sein:'
         send_preview: Vorschau an %{email} senden
         send_to_all:
           one: "%{display_count} E-Mail senden"
@@ -967,9 +1000,9 @@ de:
         allow: Link erlauben
         allow_provider: Herausgeber*in erlauben
         confirm_allow: Möchtest du die ausgewählten Links wirklich erlauben?
-        confirm_allow_provider: Möchtest du die ausgewählten Provider wirklich erlauben?
+        confirm_allow_provider: Möchtest du die ausgewählten Anbieter wirklich erlauben?
         confirm_disallow: Möchtest du die ausgewählten Links wirklich verbieten?
-        confirm_disallow_provider: Möchtest du die ausgewählten Provider wirklich verbieten?
+        confirm_disallow_provider: Möchtest du die ausgewählten Anbieter wirklich verbieten?
         description_html: Dies sind Links, die derzeit von zahlreichen Konten geteilt werden und die deinem Server aufgefallen sind. Die Benutzer*innen können darüber herausfinden, was in der Welt vor sich geht. Die Links werden allerdings erst dann öffentlich vorgeschlagen, wenn du die Herausgeber*innen genehmigt hast. Du kannst alternativ aber auch nur einzelne URLs zulassen oder ablehnen.
         disallow: Link verbieten
         disallow_provider: Herausgeber*in verbieten
@@ -1904,6 +1937,10 @@ de:
     recovery_instructions_html: Falls du jemals den Zugang zu deinem Smartphone verlierst, kannst du einen der unten aufgeführten Wiederherstellungscodes verwenden, um wieder Zugang zu deinem Konto zu erhalten. <strong>Bewahre die Wiederherstellungscodes sicher auf</strong>. Du kannst sie zum Beispiel ausdrucken und zusammen mit anderen wichtigen Dokumenten aufbewahren.
     webauthn: Sicherheitsschlüssel
   user_mailer:
+    announcement_published:
+      description: 'Ankündigung der Administrator*innen von %{domain}:'
+      subject: Ankündigung
+      title: Ankündigung von %{domain}
     appeal_approved:
       action: Kontoeinstellungen
       explanation: Der Einspruch gegen deinen Verstoß vom %{strike_date}, den du am %{appeal_date} eingereicht hast, wurde genehmigt. Dein Konto ist wieder in Ordnung.
@@ -1936,8 +1973,8 @@ de:
     terms_of_service_changed:
       agreement: Wenn du %{domain} weiterhin verwendest, stimmst du den neuen Nutzungsbedingungen automatisch zu. Falls du mit diesen nicht einverstanden bist, kannst du die Vereinbarung mit %{domain} jederzeit widerrufen, indem du dein Konto dort löschst.
       changelog: 'Hier siehst du, was sich geändert hat:'
-      description: 'Du erhältst diese E-Mail, weil wir einige Änderungen an unseren Nutzungsbedingungen für %{domain} vorgenommen haben. Wir empfehlen, die vollständig aktualisierte Fassung hier zu lesen:'
-      description_html: Du erhältst diese E-Mail, weil wir einige Änderungen an unseren Nutzungsbedingungen für %{domain} vorgenommen haben. Wir empfehlen, die <a href="%{path}" target="_blank">vollständig aktualisierte Fassung hier zu lesen</a>.
+      description: 'Du erhältst diese E-Mail, weil wir einige Änderungen an unseren Nutzungsbedingungen für %{domain} vorgenommen haben. Diese Änderungen gelten ab %{date}. Wir empfehlen, die vollständig aktualisierte Fassung hier zu lesen:'
+      description_html: Du erhältst diese E-Mail, weil wir einige Änderungen an unseren Nutzungsbedingungen für %{domain} vorgenommen haben. Diese Änderungen gelten ab <strong>%{date}</strong>. Wir empfehlen, die <a href="%{path}" target="_blank">vollständig aktualisierte Fassung hier zu lesen</a>.
       sign_off: Das Team von %{domain}
       subject: Aktualisierungen unserer Nutzungsbedingungen
       subtitle: Die Nutzungsbedingungen von %{domain} ändern sich
diff --git a/config/locales/devise.az.yml b/config/locales/devise.az.yml
index e9ba86bc79..6a02cd7e49 100644
--- a/config/locales/devise.az.yml
+++ b/config/locales/devise.az.yml
@@ -1 +1,46 @@
+---
 az:
+  devise:
+    confirmations:
+      confirmed: E-poçt ünvanınız uğurla təsdiqləndi.
+      send_instructions: Bir neçə dəqiqə ərzində e-poçt ünvanınızı necə təsdiqləyəcəyinizə dair təlimatları olan bir e-məktub alacaqsınız. Bu e-məktubu almamısınızsa, spam qovluğunuzu yoxlayın.
+      send_paranoid_instructions: E-poçt ünvanınız verilənlər bazamızda varsa, bir neçə dəqiqədən sonra e-poçt ünvanınızı necə təsdiqləyəcəyinizə dair təlimatları olan bir e-məktub alacaqsınız. Bu e-məktubu almamısınızsa, spam qovluğunuzu yoxlayın.
+    failure:
+      already_authenticated: Siz artıq daxil olmusunuz.
+      inactive: Hesabınız hələ aktivləşdirilməyib.
+      invalid: Səhv %{authentication_keys} və ya parol.
+      last_attempt: Hesabınız blok olmamışdan əvvəl bir dəfə də cəhdiniz var.
+      locked: Hesabınız bloklandı.
+      not_found_in_database: Səhv %{authentication_keys} və ya parol.
+      omniauth_user_creation_failure: Bu kimlik üçün hesab yaradarkən xəta.
+      pending: Hesabınız hələ yoxlanışdadır.
+      timeout: Sessiyanın vaxtı bitdi. Xahiş edirik davam etmək üçün yenidən daxil olun.
+      unauthenticated: Davam etmək üçün daxil olmaq və ya qeydiyyatdan keçmək lazımdır.
+      unconfirmed: Davam etmək üçün e-poçt ünvanınızı təsdiqləməlisiniz.
+    mailer:
+      confirmation_instructions:
+        action: E-poçt ünvanını təsdiqlə
+        action_with_app: Təsdiqlə və %{app}-a geri qayıt
+        explanation: Siz %{host} saytında bu e-poçt ilə hesab yaratmısınız. Onu aktivləşdirməkdən bir klik uzaqlıqdasınız. Əgər bu siz olmamısınızsa, zəhmət olmasa, bu e-məktuba məhəl qoymayın.
+        explanation_when_pending: Bu e-poçt ünvanı ilə %{host} saytına dəvət üçün müraciət etmisiniz. Siz e-poçt ünvanınızı təsdiqlədikdən sonra müraciətinizi nəzərdən keçirəcəyik. Siz məlumatlarınızı dəyişdirmək və ya hesabınızı silmək üçün daxil ola bilərsiniz, lakin hesabınız təsdiqlənənə qədər əksər funksiyaları istifadə edə bilməzsiniz. Müraciətiniz rədd edilərsə, məlumatlarınız silinəcək, buna görə də sizdən heç bir tədbir tələb olunmayacaq. Əgər bu siz deyildinizsə, zəhmət, bu e-məktuba məhəl qoymayın.
+        extra_html: Həmçinin zəhmət olmasa, <a href="%{terms_path}">serverin qaydalarını</a> və <a href="%{policy_path}">istifadə şərtlərini</a> oxuyun.
+        subject: 'Mastodon: %{instance} üçün təsdiqlənmə təlimatları'
+        title: E-poçt ünvanını təsdiqlə
+      email_changed:
+        explanation: 'Hesabınız üçün e-poçt ünvanı buna dəyişdirilir:'
+        extra: E-poçtunuzu dəyişməmisinizsə, çox güman ki, kimsə hesabınıza giriş əldə edib. Zəhmət olmasa, parolunuzu dərhal dəyişdirin və ya hesabınıza daxil ola bilməyəcəksinizsə, server admini ilə əlaqə saxlayın.
+        subject: 'Mastodon: E-poçt dəyişdirildi'
+        title: Yeni e-poçt ünvanı
+      password_change:
+        explanation: Hesabınızın parolu dəyişdirilib.
+        extra: Parolunuzu dəyişməmisinizsə, çox güman ki, kimsə hesabınıza giriş əldə edib. Zəhmət olmasa, parolunuzu dərhal dəyişdirin və ya hesabınıza daxil ola bilməyəcəksinizsə, server admini ilə əlaqə saxlayın.
+        subject: 'Mastodon: Parol dəyişdirildi'
+        title: Parol dəyişdirildi
+      reconfirmation_instructions:
+        explanation: E-poçtunuzu dəyişdirmək üçün yeni ünvanı təsdiqləyin.
+        extra: Əgər bu dəyişiklik sizin tərəfinizdən deyilsə, zəhmət olmasa, bu e-məktuba məhəl qoymayın. Siz yuxarıdakı linkə daxil olana qədər Mastodon hesabının e-poçt ünvanı dəyişməyəcək.
+        subject: 'Mastodon: %{instance} üçün e-poçtu təsdiqlə'
+        title: E-poçt ünvanını təsdiqlə
+      reset_password_instructions:
+        action: Parolu dəyiş
+        explanation: Siz hesabınız üçün yeni parol tələb etmisiniz.
diff --git a/config/locales/devise.fa.yml b/config/locales/devise.fa.yml
index 20d7ec7503..71cd7699fe 100644
--- a/config/locales/devise.fa.yml
+++ b/config/locales/devise.fa.yml
@@ -16,13 +16,13 @@ fa:
       pending: حساب شما همچنان در دست بررسی است.
       timeout: مهلت این ورود شما به سر رسید. برای ادامه، دوباره وارد شوید.
       unauthenticated: برای ادامه باید وارد شوید یا ثبت نام کنید.
-      unconfirmed: برای ادامه باید نشانی ایمیل خود را تأیید کنید.
+      unconfirmed: پیش از ادامه باید نشانی رایانامه‌تان را تأیید کنید.
     mailer:
       confirmation_instructions:
-        action: تأیید نشانی ایمیل
+        action: تأیید نشانی رایانامه
         action_with_app: پذیرش و بازگشت به %{app}
-        explanation: شما با این نشانی ایمیل حسابی در %{host} باز کرده‌اید. با یک کلیک می‌توانید این حساب را فعال کنید. اگر شما چنین کاری نکردید، لطفاً این ایمیل را نادیده بگیرید.
-        explanation_when_pending: شما با این نشانی ایمیل برای %{host} درخواست دعوت‌نامه داده‌اید. اگر ایمیل خود را تأیید کنید، ما درخواست شما را بررسی خواهیم کرد. تا وقتی بررسی تمام نشده، شما نمی‌توانید به حساب خود وارد شوید. اگر درخواست شما رد شود، ما اطلاعاتی را که از شما داریم پاک خواهیم کرد پس نیازی به کاری از سمت شما نخواهد بود. اگر شما چنین درخواستی نداده‌اید، لطفاً این ایمیل را نادیده بگیرید.
+        explanation: حسابی با این نشانی رایانامه روی %{host} ساخته‌اید. یک کلیک با فعّال کردنش فاصله دارید. اگر شما نبودید از این رایانامه چشم بپوشید.
+        explanation_when_pending: درخواستی برای دعوت به %{host} با این نشانی رایانامه داده‌اید. درخواستتان را پس از تأیید نشانی رایانامه‌تان بررسی خواهیم کرد. می‌توانید برای تغییر جزییات یا حذف حسابتان وارد شوید؛ ولی تا پیش از تأیید حساب نمی‌توانید به بیش‌تر قابلیت‌ها دسترسی داشته باشید. داده‌هایتان در صورت رد شدن درخواست برداشته خواهد شد و نیازی به اقدامی از سوی شما نیست. اگر شما نبودید از این رایانامه چشم بپوشید.
         extra_html: لطفاً همچنین <a href="%{terms_path}">قوانین کارساز</a> و <a href="%{policy_path}">شرایط خدمتمان</a> را بررسی کنید.
         subject: 'ماستودون: دستورالعمل تأیید برای %{instance}'
         title: تأیید نشانی رایانامه
@@ -87,22 +87,22 @@ fa:
       failure: تآیید هویتتان از %{kind} نتوانست انجام شود چرا که «%{reason}».
       success: تأیید هویت از حساب %{kind} با موفقیت انجام شد.
     passwords:
-      no_token: این صفحه را تنها از راه یک ایمیل بازنشانی گذرواژه می‌شود دید. اگر از چنین ایمیلی می‌آیید، لطفاً مطمئن شوید که نشانی موجود در ایمیل را کامل به کار برده‌اید.
-      send_instructions: اگر ایمیل شما در پایگاه دادهٔ ما موجود باشد، تا دقایقی دیگر یک ایمیل بازیابی گذرواژه دریافت خواهید کرد. اگر این ایمیل نیامد، لطفاً پوشهٔ هرزنامه‌هایتان را بررسی کنید.
-      send_paranoid_instructions: اگر ایمیل شما در پایگاه دادهٔ ما موجود باشد، تا دقایقی دیگر یک ایمیل بازیابی گذرواژه دریافت خواهید کرد. اگر این ایمیل نیامد، لطفاً پوشهٔ هرزنامه‌هایتان را بررسی کنید.
+      no_token: بدون آمدن از رایانامهٔ بازنشانی گذرواژه نمی‌توان به این صفحه دسترسی داشت. اگر از چنین رایانامه‌ای آمده‌اید لطفاً از استفادهٔ نشانی کامل مطمئن شوید.
+      send_instructions: اگر نشانی رایانامه‌تان در پایگاه داده‌مان باشد تا چند دقیقهٔ دیگر پیوند بازیابی گذرواژه‌ای در آن خواهید گرفت. بررسی شاخهٔ هرزنامه در صورت نگرفتن این رایانامه.
+      send_paranoid_instructions: اگر نشانی رایانامه‌تان در پایگاه داده‌مان باشد تا چند دقیقهٔ دیگر پیوند بازیابی گذرواژه‌ای در آن خواهید گرفت. بررسی شاخهٔ هرزنامه در صورت نگرفتن این رایانامه.
       updated: گذرواژه شما با موفقیت تغییر کرد. شما الان وارد سیستم هستید.
       updated_not_active: گذرواژه شما با موفقیت تغییر کرد.
     registrations:
       destroyed: بدرود! حساب شما با موفقیت لغو شد. امیدواریم دوباره شما را ببینیم.
-      update_needs_confirmation: حسابتان را با موفّقیت به‌روز کردید؛‌ ولی باید نشانی رایانامهٔ جدیتان را تأیید کنیم. لطفاً رایانامه‌تان را بررسی کرده و برای تأیید نشانی رایانهٔ جدیدتان پیوند را بزنید. اگر این رایانامه را نگرفته‌اید شاخهٔ هرزنامه‌ها را بررسی کنید.
+      update_needs_confirmation: حسابتان را با موفّقیت به‌روز کردید؛ ولی نشانی رایانامهٔ جدیدتان باید تأیید شود. بررسی رایانامه و پیروی از پیوند تأییدیه برای تأیید نشانی رایانهٔ جدیدتان. بررسی شاخهٔ هرزنامه در صورت نگرفتن این رایانامه.
       updated: حسابتان با موفّقیت به‌روز شد.
     sessions:
       already_signed_out: با موفّقیت خارج شدید.
       signed_in: با موفّقیت وارد شدید.
       signed_out: با موفّقیت خارج شدید.
     unlocks:
-      send_instructions: تا دقایقی دیگر رایانامه‌ای با دستورالعمل قفل‌گشایی حسابتان دریافت خواهید کرد. اگر این رایانامه را نگرفتید، لطفاً پوشهٔ هرزنامه‌هایتان را بررسی کنید.
-      send_paranoid_instructions: اگر حسابتان وجود داشته باشد تا دقایقی دیگر رایانامه‌ای با دستورالعمل قفل‌گشاییش دریافت خواهید کرد. اگر این رایانامه را نگرفتید، لطفاً پوشهٔ هرزنامه‌هایتان را بررسی کنید.
+      send_instructions: تا دقایقی دیگر رایانامه‌ای با دستورالعمل قفل‌گشایی حسابتان دریافت خواهید کرد. بررسی شاخهٔ هرزنامه در صورت نگرفتن این رایانامه.
+      send_paranoid_instructions: اگر حسابتان وجود داشته باشد تا دقایقی دیگر رایانامه‌ای با دستورالعمل قفل‌گشاییش دریافت خواهید کرد. بررسی شاخهٔ هرزنامه در صورت نگرفتن این رایانامه.
       unlocked: حسابتان با موفّقیت قفل‌گشایی شد. لطفاً برای ادامه وارد شوید.
   errors:
     messages:
diff --git a/config/locales/devise.gl.yml b/config/locales/devise.gl.yml
index 689d78f307..6bbec181b8 100644
--- a/config/locales/devise.gl.yml
+++ b/config/locales/devise.gl.yml
@@ -2,8 +2,8 @@
 gl:
   devise:
     confirmations:
-      confirmed: O teu enderezo de email foi confirmado.
-      send_instructions: Vas recibir un email coas instrucións para confirmar o teu enderezo de email dentro dalgúns minutos. Por favor, comproba o cartafol de spam se non recibiches o correo.
+      confirmed: Confirmouse correctamente o teu enderezo de correo.
+      send_instructions: Nuns minutos vas recibir un correo coas instrucións para confirmar o teu enderezo de correo. Por favor, comproba o cartafol de spam se non recibiches o correo.
       send_paranoid_instructions: Se o teu enderezo de email xa existira na nosa base de datos, vas recibir un correo coas instrucións de confirmación dentro dalgúns minutos. Por favor, comproba o cartafol de spam se non recibiches o correo.
     failure:
       already_authenticated: Xa estás conectada.
@@ -16,31 +16,31 @@ gl:
       pending: A túa conta aínda está baixo revisión.
       timeout: A túa sesión caducou. Accede outra vez para continuar.
       unauthenticated: Precisas iniciar sesión ou rexistrarte antes de continuar.
-      unconfirmed: Tes que confirmar o teu enderezo de email antes de continuar.
+      unconfirmed: Tes que confirmar o teu enderezo de correo antes de continuar.
     mailer:
       confirmation_instructions:
-        action: Verificar o enderezo de email
+        action: Verificar o enderezo de correo
         action_with_app: Confirmar e volver a %{app}
-        explanation: Creaches unha conta en %{host} con este enderezo de email. Estás a un clic de activala. Se non foches ti o que fixeches este rexisto, por favor ignora esta mensaxe.
-        explanation_when_pending: Solicitaches un convite para %{host} con este enderezo de email. Logo de que confirmes o teu enderezo de email, imos revisar a túa inscrición. Podes iniciar sesión para mudar os teus datos ou eliminar a túa conta, mais non poderás aceder á meirande parte das funcións até que a túa conta sexa aprobada. Se a túa inscrición for rexeitada, os teus datos serán eliminados, polo que non será necesaria calquera acción adicional da túa parte. Se non solicitaches este convite, por favor, ignora este correo.
+        explanation: Creaches unha conta en %{host} con este enderezo de correo. Estás a un clic de activala. Se non foches ti quen fixo esta solicitude, por favor ignora esta mensaxe.
+        explanation_when_pending: Solicitaches un convite para %{host} con este enderezo de correo. Logo confirmes o teu enderezo de correo, imos revisar a túa inscrición. Podes iniciar sesión para mudar os teus datos ou eliminar a túa conta, mais non poderás aceder á meirande parte das funcións até que a túa conta sexa aprobada. Se a túa inscrición for rexeitada, os teus datos serán eliminados, polo que non será necesaria calquera acción adicional da túa parte. Se non solicitaches este convite, por favor, ignora este correo.
         extra_html: Por favor, le <a href="%{terms_path}">as regras do servidor</a> e os <a href="%{policy_path}">nosos termos do servizo</a>.
         subject: 'Mastodon: Instrucións de confirmación para %{instance}'
-        title: Verificar o enderezo de email
+        title: Verificar o enderezo de correo
       email_changed:
         explanation: 'O email asociado á túa conta será mudado a:'
         extra: Se non mudaches o teu email é posíbel que alguén teña conseguido acceder á túa conta. Por favor muda o teu contrasinal de xeito imediato ou entra en contacto cun administrador do servidor se non podes acceder a túa conta.
         subject: 'Mastodon: Email mudado'
-        title: Novo enderezo de email
+        title: Novo enderezo de correo
       password_change:
         explanation: O contrasinal da túa conta foi mudado.
         extra: Se non mudaches o teu contrasinal, é posíbel que alguén teña conseguido acceder á túa conta. Por favor muda o teu contrasinal de xeito imediato ou entra en contato cun administrador do servidor se non podes acceder a túa conta.
         subject: 'Mastodon: Contrasinal mudado'
         title: Contrasinal mudado
       reconfirmation_instructions:
-        explanation: Confirma o teu novo enderezo para mudar o email.
-        extra: Se esta mudanza non foi comezada por ti, por favor ignora este email. O enderezo de email para a túa conta do Mastodon non mudará mentres non accedas á ligazón de enriba.
+        explanation: Confirma o teu novo enderezo para mudar o correo.
+        extra: Se esta mudanza non foi iniciada por ti, por favor ignora este correo. O enderezo de correo para a túa conta do Mastodon non mudará mentres non accedas á ligazón de enriba.
         subject: 'Mastodon: Confirmar email para %{instance}'
-        title: Verificar o enderezo de email
+        title: Verificar o enderezo de correo
       reset_password_instructions:
         action: Mudar contrasinal
         explanation: Solicitaches un novo contrasinal para a túa conta.
@@ -87,14 +87,14 @@ gl:
       failure: Non foi posible autenticarte desde %{kind} debido a "%{reason}".
       success: Autenticado con éxito na conta %{kind}.
     passwords:
-      no_token: Non podes acceder a esta páxina se non vés a través da ligazón enviada por email para o mudado do teu contrasinal. Se empregaches esa ligazón para chegar aquí, por favor verifica que o enderezo URL actual é o mesmo do que foi enviado no email.
-      send_instructions: Se o teu enderezo de email existe na nosa base de datos, vas recibir un email coas instrucións para mudar o contrasinal dentro duns minutos. Por favor, comproba o teu cartafol de correo lixo (spam) se ves que non recibiches o email.
-      send_paranoid_instructions: Se o teu enderezo de email existe na nosa base de datos, vas recibir unha ligazón para recuperar o contrasinal dentro duns minutos. Por favor, comproba o teu cartafol de correo lixo (spam) se ves que non recibiches o email.
+      no_token: Non podes acceder a esta páxina se non vés a través da ligazón enviada por correo para o cambio do contrasinal. Se empregaches esa ligazón para chegar aquí, por favor verifica que o enderezo URL actual é o mesmo do que foi enviado no correo.
+      send_instructions: Se o teu enderezo de correo existe na nosa base de datos, vas recibir un email coas instrucións para mudar o contrasinal dentro duns minutos. Por favor, comproba o teu cartafol de correo lixo (spam) se ves que non recibiches o correo.
+      send_paranoid_instructions: Se o teu enderezo de correo existe na nosa base de datos, vas recibir un email coas instrucións para mudar o contrasinal dentro duns minutos. Por favor, comproba o teu cartafol de correo lixo (spam) se ves que non recibiches o correo.
       updated: O teu contrasinal foi mudado. Estás xa autenticado na túa conta.
       updated_not_active: O teu contrasinal foi mudado de xeito correcto.
     registrations:
       destroyed: Adeus! A túa conta foi cancelada de xeito correcto. Agardamos verte de novo.
-      update_needs_confirmation: Actualizaches a túa conta de xeito correcto, pero precisamos verificar o teu novo enderezo de email. Por favor, revisa o teu email e segue a ligazón para confirmar o teu novo enderezo de email. Comproba o teu cartafol de correo lixo (spam) se ves que non recibiches o correo.
+      update_needs_confirmation: Actualizaches a túa conta de xeito correcto, pero precisamos verificar o teu novo enderezo de correo. Por favor, revisa o teu correo e segue a ligazón para confirmar o teu novo enderezo de correo. Comproba o cartafol de correo lixo (spam) se ves que non recibiches o correo.
       updated: A túa conta foi actualizada de xeito correcto.
     sessions:
       already_signed_out: Pechouse a sesión de xeito correcto.
diff --git a/config/locales/devise.io.yml b/config/locales/devise.io.yml
index feef32bf67..8a86cf18b0 100644
--- a/config/locales/devise.io.yml
+++ b/config/locales/devise.io.yml
@@ -21,19 +21,19 @@ io:
       confirmation_instructions:
         action: Verifikez retpostadreso
         action_with_app: Konfirmez e retrovenez a %{app}
-        explanation: Vu kreis konto che %{host} per ca retpostadreso. Vu povas facile aktivigar lu. Se vu ne agis lu, ignorez ca retposto.
+        explanation: Vu kreis konto sur %{host} per ca retpostadreso. Se vu ne agis lu, ignorez ca retposto.
         explanation_when_pending: Vu aplikis por ganar invito a %{host} per ca retpostkonto. Pos vu konfirmas vua retpostkonto, ni kontrolos vua apliko. Vu povas enirar por chanjar vua detali o efacar vua konto, ma vu ne povas acesar maxim de funcioni til vua konto aprobesas. Se vua apliko refuzesas, vua informi efacesos, do plusa ago ne bezonesos de vu. Se vu ne agis lu, ignorez ca retposto.
         extra_html: Anke videz <a href="%{terms_path}">reguli di la servilo</a> e <a href="%{policy_path}">nia servokondicioni</a>.
         subject: Instrucioni por konfirmar %{instance}
         title: Verifikez retpostadreso
       email_changed:
         explanation: 'Retpostadreso di vua konto chanjesas a:'
-        extra: Se vu ne chanjesis vua retpostadreso, nulu posible acesis vua konto. Chanjez vua pasvorto quik o kontaktez serviladministratero se vu ne povas enirar vua konto.
+        extra: Se vu ne chanjesis vua retpostadreso, eble ulu adiris vua konto.
         subject: 'Mastodon: Retpostadreso chanjesis'
         title: Nova retpostadreso
       password_change:
         explanation: La pasvorto di vua konto chanjesis.
-        extra: Se vu ne chanjesis vua pasvorto, nulu posible acesis vua konto. Chanjez vua pasvorto quik o kontaktez serviladministratero se vu ne povas enirar vua konto.
+        extra: Se vu ne chanjesis vua pasvorto, eble ulu adiris vua konto.
         subject: Tua pasvorto chanjesis senprobleme.
         title: Pasvorto chanjesis
       reconfirmation_instructions:
@@ -49,14 +49,14 @@ io:
         title: Richanjo di pasvorto
       two_factor_disabled:
         explanation: Eniro esas nun posibla per nur retpostoadreso e pasvorto.
-        subject: 'Mastodon: 2-faktorverifiko deaktivigesis'
-        subtitle: 2-faktora verifiko por vua konto desaktivigesis.
-        title: 2FA deaktivigesis
+        subject: 'Mastodon: Dufaktora yurizo desebligesis'
+        subtitle: Dufaktora yurizo por vua konto desebligesis.
+        title: 2FA desebligesis
       two_factor_enabled:
         explanation: Ficho facesis da parigita softwaro TOTP bezonesos por eniro.
-        subject: 'Mastodon: 2-faktorverifiko aktivigesis'
-        subtitle: 2-faktora verifiko aktivigesis por vua konto.
-        title: 2FA aktivigesis
+        subject: 'Mastodon: Dufaktora yurizo ebligesis'
+        subtitle: Dufaktora yurizo ebligesis por vua konto.
+        title: 2FA ebligesis
       two_factor_recovery_codes_changed:
         explanation: Antea rigankodexi devalidesis e novo facesis.
         subject: 'Mastodon: 2-faktorrigankodexi rifacesis'
@@ -66,23 +66,23 @@ io:
         subject: Instructioni por riacendar la konto
       webauthn_credential:
         added:
-          explanation: Ca sekurklefo insertesis a vua konto
+          explanation: Ca sekuresklefo adjuntesis ad vua konto
           subject: 'Mastodon: Nova sekurklefo'
-          title: Nova sekurklefo insertesis
+          title: Nova sekuresklefo adjuntesis
         deleted:
           explanation: Ca sekurklefo efacesis de vua konto
           subject: 'Mastodon: Sekurklefo efacesis'
           title: 1 de vua sekurklefi efacesis
       webauthn_disabled:
-        explanation: Verifiko per sekuresklefi desaktivigesis por vua konto.
+        explanation: Yurizo kun sekuresklefi desebligesis por vua konto.
         extra: Eniro esas nun posibla per nur ficho qua facesis da parigita softwaro TOTP.
-        subject: 'Mastodon: Verifiko per sekurklefi deaktivigesis'
-        title: Sekurklefi deaktivigesis
+        subject: 'Mastodon: Yurizo per sekuresklefi desebligesis'
+        title: Sekuresklefi desebligesis
       webauthn_enabled:
-        explanation: Sekuresklefoa verifiko aktivigesis por vua konto.
+        explanation: Sekuresklefyurizo ebligesis por vua konto.
         extra: Vua sekuresklefo povas nun uzesar por eniro.
-        subject: 'Mastodon: Sekurklefverifiko aktivigesis'
-        title: Sekurklefi aktivigesis
+        subject: 'Mastodon: Sekuresklefyurizo ebligesis'
+        title: Sekuresklefi ebligesis
     omniauth_callbacks:
       failure: 'Ni ne povis autentikigar tu per %{kind}: ''%{reason}''.'
       success: Autentikigita senprobleme per %{kind}.
diff --git a/config/locales/devise.kab.yml b/config/locales/devise.kab.yml
index 4638afc8e0..1628643bd1 100644
--- a/config/locales/devise.kab.yml
+++ b/config/locales/devise.kab.yml
@@ -88,8 +88,8 @@ kab:
       success: Asesṭeb idda akken iwata seg umiḍan %{kind}.
     passwords:
       no_token: Ur tezmireḍ ara ad tkecmeḍ ɣer usebter-a war ma tusiḍ-d seg imayl n uwennez n wawal uffir. ma syin i d-tusiḍ, wali ma tesqedceḍ tansa URL i d ak·am-d-nuzen.
-      send_instructions: Ma nufa tansa-inek imayl tella deg uzadur-nneγ n yisefka, ad n-teṭṭfeḍ izen deg kra n tesdatin, deg-s assaγ i uɛawed n wawal uffir. Ma ur k-in-yewwiḍ ara yizen, ttxil-k ẓer deg ukaram spam.
-      send_paranoid_instructions: Ma nufa tansa-inek imayl tella deg uzadur-nneγ n yisefka, ad n-teṭṭfeḍ izen deg kra n tesdatin, deg-s assaγ i uɛawed n wawal uffir. Ma ur k-in-yewwiḍ ara yizen, ttxil-k ẓer deg ukaram spam.
+      send_instructions: Ma nufa tansa-inek·inem imayl tella deg uzadur-nneɣ n yisefka, ad n-teṭṭfeḍ izen deg kra n tesdatin, deg-s assaɣ i uɛawed n wawal uffir. Ma ur k-in-yewwiḍ ara yizen, ttxil-k·m ẓer deg ukaram yespamen.
+      send_paranoid_instructions: Ma nufa tansa-inek·inem imayl tella deg uzadur-nneɣ n yisefka, ad n-teṭṭfeḍ izen deg kra n tesdatin, deg-s assaɣ i uɛawed n wawal uffir. Ma ur k-in-yewwiḍ ara yizen, ttxil-k·m ẓer deg ukaram yespamen.
       updated: Awal-ik uffir yettwabeddel mebla ugur. Aqla-k tura tjerrḍeḍ.
       updated_not_active: Awal-ik uffir yettwabeddel mebla ugur.
     registrations:
diff --git a/config/locales/devise.lv.yml b/config/locales/devise.lv.yml
index 9fbd99c0eb..e60f8fa62e 100644
--- a/config/locales/devise.lv.yml
+++ b/config/locales/devise.lv.yml
@@ -2,9 +2,9 @@
 lv:
   devise:
     confirmations:
-      confirmed: Tava e-pasta adrese ir veiksmīgi apstiprināta.
-      send_instructions: Pēc dažām minūtēm saņemsi e-pastu ar norādījumiem, kā apstiprināt savu e-pasta adresi. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
-      send_paranoid_instructions: Ja tava e-pasta adrese ir mūsu datu bāzē, pēc dažām minūtēm saņemsi e-pastu ar norādījumiem, kā apstiprināt savu e-pasta adresi. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
+      confirmed: Tava e-pasta adrese tika sekmīgi apstiprināta.
+      send_instructions: Pēc dažām minūtēm saņemsi e-pasta ziņojum ar norādēm, kā apstiprināt savu e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
+      send_paranoid_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm saņemsi e-pasta ziņojumu ar norādēm, kā apstiprināt savu e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
     failure:
       already_authenticated: Tu jau esi pieteicies.
       inactive: Tavs konts vēl nav aktivizēts.
@@ -58,10 +58,10 @@ lv:
         subtitle: Tavam kontam tika iespējota divpakāpju autentifikācija.
         title: 2FA iespējota
       two_factor_recovery_codes_changed:
-        explanation: Iepriekšējie atkopšanas kodi ir atzīti par nederīgiem un ģenerēti jauni.
-        subject: 'Mastodon: Divfaktoru atkopšanas kodi pārģenerēti'
-        subtitle: Iepriekšējie atkopšanas kodi tika padarīti par nederīgiem, un tika izveidoti jauni.
-        title: 2FA atkopšanas kodi mainīti
+        explanation: Iepriekšējie atkopes kodi ir padarīt nederīgi, un ir izveidoti jauni.
+        subject: 'Mastodon: atkārtoti izveidoti divpakāpju atkopes kodi'
+        subtitle: Iepriekšējie atkopes kodi tika padarīti par nederīgiem, un tika izveidoti jauni.
+        title: 2FA atkopes kodi nomainīti
       unlock_instructions:
         subject: 'Mastodon: Norādījumi atbloķēšanai'
       webauthn_credential:
@@ -85,25 +85,25 @@ lv:
         title: Drošības atslēgas iespējotas
     omniauth_callbacks:
       failure: Nevarēja autentificēt tevi no %{kind}, jo "%{reason}".
-      success: Veiksmīgi autentificēts no %{kind} konta.
+      success: Sekmīgi autentificēts no %{kind} konta.
     passwords:
       no_token: Tu nevari piekļūt šai lapai, ja neesi saņēmis paroles atiestatīšanas e-pasta ziņojumu. Ja ienāci no paroles atiestatīšanas e-pasta, lūdzu, pārliecinies, vai izmanto visu norādīto URL.
-      send_instructions: Ja tava e-pasta adrese ir mūsu datu bāzē, pēc dažām minūtēm uz savu e-pasta adresi saņemsi paroles atkopšanas saiti. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
-      send_paranoid_instructions: Ja tava e-pasta adrese ir mūsu datu bāzē, pēc dažām minūtēm uz savu e-pasta adresi saņemsi paroles atkopšanas saiti. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
-      updated: Tava parole tika veiksmīgi nomainīta. Tagad esi pieteicies.
-      updated_not_active: Tava parole ir veiksmīgi nomainīta.
+      send_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm savā e-pasta adresē saņemsi paroles atkopes saiti. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
+      send_paranoid_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm savā e-pasta adresē saņemsi paroles atkopes saiti. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
+      updated: Tava parole tika sekmīgi nomainīta. Tagad esi pieteicies.
+      updated_not_active: Tava parole tika sekmīgi nomainīta.
     registrations:
-      destroyed: Visu labu! Tavs konts ir veiksmīgi atcelts. Mēs ceram tevi drīz atkal redzēt.
-      update_needs_confirmation: Tu veiksmīgi atjaunināji savu kontu, taču mums ir jāverificē teva jaunā e-pasta adrese. Lūdzu, pārbaudi savu e-pastu un seko apstiprinājuma saitei, lai apstiprinātu savu jauno e-pasta adresi. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
-      updated: Tavs konts ir veiksmīgi atjaunināts.
+      destroyed: Visu labu! Tavs konts ir sekmīgi atcelts. Mēs ceram Tevi drīz atkal redzēt.
+      update_needs_confirmation: Tu sekmīgi atjaunināji savu kontu, taču mums ir jāapliecina Tava jaunā e-pasta adrese. Lūgums pārbaudīt savu e-pastu un sekot apstiprinājuma saitei, lai apstiprinātu savu jauno e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
+      updated: Tavs konts tika sekmīgi atjaunināts.
     sessions:
-      already_signed_out: Veiksmīgi izrakstījies.
-      signed_in: Veiksmīgi pieteicies.
-      signed_out: Veiksmīgi izrakstījies.
+      already_signed_out: Sekmīgi izrakstījies.
+      signed_in: Sekmīgi pierakstījies.
+      signed_out: Sekmīgi izrakstījies.
     unlocks:
-      send_instructions: Pēc dažām minūtēm tu saņemsi e-pastu ar norādījumiem, kā atbloķēt savu kontu. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
-      send_paranoid_instructions: Ja tavs konts eksistē, dažu minūšu laikā tu saņemsi e-pastu ar norādījumiem, kā to atbloķēt. Lūdzu, pārbaudi spama mapi, ja neesi saņēmis šo e-pastu.
-      unlocked: Konts tika veiksmīgi atbloķēts. Lūgums pieteikties, lai turpinātu.
+      send_instructions: Pēc dažām minūtēm Tu saņemsi e-pasta ziņojumu ar norādēm, kā atslēgt savu kontu. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
+      send_paranoid_instructions: Ja Tavs konts pastāv, dažu minūšu laikā saņemsi e-pasta ziņojumu ar norādēm, kā to atslēgt. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
+      unlocked: Konts tika sekmīgi atslēgts. Lūgums pieteikties, lai turpinātu.
   errors:
     messages:
       already_confirmed: jau tika apstiprināts, lūgums mēģināt pieteikties
diff --git a/config/locales/devise.pt-BR.yml b/config/locales/devise.pt-BR.yml
index 6c33d0e30a..aa1190dfd0 100644
--- a/config/locales/devise.pt-BR.yml
+++ b/config/locales/devise.pt-BR.yml
@@ -14,7 +14,7 @@ pt-BR:
       not_found_in_database: "%{authentication_keys} ou senha inválida."
       omniauth_user_creation_failure: Erro ao criar uma conta para esta identidade.
       pending: Sua conta está sendo revisada.
-      timeout: Sua sessão expirou. Faça ‘login’ novamente para continuar.
+      timeout: Sua sessão expirou. Por favor, entre novamente para continuar.
       unauthenticated: Você precisa entrar ou criar uma conta antes de continuar.
       unconfirmed: Você precisa confirmar o seu endereço de e-mail antes de continuar.
     mailer:
@@ -48,12 +48,12 @@ pt-BR:
         subject: 'Mastodon: Instruções para alterar senha'
         title: Redefinir senha
       two_factor_disabled:
-        explanation: O ‘login’ agora é possível usando apenas o endereço eletrônico e senha.
+        explanation: Agora você pode entrar usando apenas seu e-mail e senha.
         subject: 'Mastodon: Autenticação de dois fatores desativada'
         subtitle: A autenticação de dois fatores foi desativada.
         title: 2FA desativada
       two_factor_enabled:
-        explanation: Será necessário um código gerado pelo aplicativo de autenticação TOTP para fazer login.
+        explanation: Um código de autenticação de dois fatores será necessário para entrar em sua conta.
         subject: 'Mastodon: Autenticação de dois fatores ativada'
         subtitle: A autenticação de dois fatores foi ativada para sua conta.
         title: 2FA ativada
@@ -75,11 +75,11 @@ pt-BR:
           title: Uma das suas chaves de segurança foi excluída
       webauthn_disabled:
         explanation: A autenticação por chaves de segurança foi desativada para sua conta.
-        extra: Agora você pode fazer login usando apenas o código gerado pelo aplicativo de autenticação TOTP.
+        extra: Agora você pode entrar usando apenas o código de autenticação de dois fatores.
         subject: 'Mastodon: Autenticação por chaves de segurança desativada'
         title: Chaves de segurança desativadas
       webauthn_enabled:
-        explanation: A autenticação por chave de segurança foi ativada para sua conta.
+        explanation: A autenticação por chave de segurança foi ativada.
         extra: Sua chave de segurança agora pode ser usada para ‘login’.
         subject: 'Mastodon: Autenticação por chaves de segurança ativada'
         title: Chaves de segurança ativadas
diff --git a/config/locales/devise.tt.yml b/config/locales/devise.tt.yml
index b9ac2e09f6..608d7dbcc9 100644
--- a/config/locales/devise.tt.yml
+++ b/config/locales/devise.tt.yml
@@ -3,7 +3,19 @@ tt:
   devise:
     confirmations:
       confirmed: Сезнең э. почта адресыгыз уңышлы расланган.
+    failure:
+      already_authenticated: Сез кердегез инде.
+      inactive: Сезнең аккаунтыгыз әле активламаган.
+      invalid: "%{authentication_keys} яки серсүз дөрес кертелмәгән."
+      locked: Сезнең хисапъязмагыз блокланган.
+      not_found_in_database: "%{authentication_keys} яки серсүз дөрес кертелмәгән."
     mailer:
+      confirmation_instructions:
+        action: Email адресын расла
+        action_with_app: Расла һәм %{app} эченә кайт
+        title: Email адресын раслагыз
+      email_changed:
+        explanation: 'Сезнең аккаунтыгызның email адресы моңа үзгәртеләчәк:'
       reset_password_instructions:
         action: Серсүзне үзгәртү
         title: Серсүзне алыштыру
diff --git a/config/locales/doorkeeper.da.yml b/config/locales/doorkeeper.da.yml
index 7ac16e0012..a10bee34df 100644
--- a/config/locales/doorkeeper.da.yml
+++ b/config/locales/doorkeeper.da.yml
@@ -130,11 +130,11 @@ da:
         crypto: Ende-til-ende kryptering
         favourites: Favoritter
         filters: Filtre
-        follow: Følger, Tavsgør og Blokerer
+        follow: Fulgte, skjjulte og blokerede kontoer
         follows: Følger
         lists: Lister
         media: Medievedhæftninger
-        mutes: Tavsgørelser
+        mutes: Skjulte kontoer
         notifications: Notifikationer
         profile: Din Mastodon-profil
         push: Push-notifikationer
@@ -177,7 +177,7 @@ da:
       read:filters: se dine filtre
       read:follows: se dine følger
       read:lists: se dine lister
-      read:mutes: se dine tavsgørelser
+      read:mutes: se dine skjulte kontoer
       read:notifications: se dine notifikationer
       read:reports: se dine anmeldelser
       read:search: søg på dine vegne
@@ -186,13 +186,13 @@ da:
       write:accounts: ændre din profil
       write:blocks: blokere konti og domæner
       write:bookmarks: bogmærke indlæg
-      write:conversations: tavsgøre og slette konversationer
+      write:conversations: skjul og slet samtaler
       write:favourites: favoritmarkere indlæg
       write:filters: oprette filtre
       write:follows: følge personer
       write:lists: oprette lister
       write:media: uploade mediefiler
-      write:mutes: tavsgøre personer og konversationer
+      write:mutes: skjul personer og samtaler
       write:notifications: rydde dine notifikationer
       write:reports: anmelde personer
       write:statuses: udgive indlæg
diff --git a/config/locales/doorkeeper.eo.yml b/config/locales/doorkeeper.eo.yml
index 36a5ed1974..ede1ef815e 100644
--- a/config/locales/doorkeeper.eo.yml
+++ b/config/locales/doorkeeper.eo.yml
@@ -191,7 +191,7 @@ eo:
       write:filters: krei filtrilojn
       write:follows: sekvi homojn
       write:lists: krei listojn
-      write:media: alŝuti plurmediojn
+      write:media: alŝuti aŭdovidaĵojn
       write:mutes: silentigi homojn kaj konversaciojn
       write:notifications: forigi viajn sciigojn
       write:reports: signali aliajn homojn
diff --git a/config/locales/doorkeeper.fi.yml b/config/locales/doorkeeper.fi.yml
index 38f88943bf..ddaff5b93a 100644
--- a/config/locales/doorkeeper.fi.yml
+++ b/config/locales/doorkeeper.fi.yml
@@ -130,8 +130,8 @@ fi:
         crypto: Päästä päähän -salaus
         favourites: Suosikit
         filters: Suodattimet
-        follow: Seuratut, mykistykset ja estot
-        follows: Seuratut
+        follow: Seurattavat, mykistykset ja estot
+        follows: Seurattavat
         lists: Listat
         media: Medialiitteet
         mutes: Mykistykset
@@ -175,7 +175,7 @@ fi:
       read:bookmarks: katso kirjanmerkkejäsi
       read:favourites: katso suosikkejasi
       read:filters: katso suodattimiasi
-      read:follows: katso seurattujasi
+      read:follows: katso seurattaviasi
       read:lists: katso listojasi
       read:mutes: katso mykistyksiäsi
       read:notifications: katso ilmoituksiasi
diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml
index 71c9605d8a..d956225dba 100644
--- a/config/locales/doorkeeper.fr.yml
+++ b/config/locales/doorkeeper.fr.yml
@@ -25,7 +25,7 @@ fr:
         edit: Modifier
         submit: Envoyer
       confirmations:
-        destroy: En êtes-vous sûr ?
+        destroy: En êtes-vous certain ?
       edit:
         title: Modifier l’application
       form:
diff --git a/config/locales/doorkeeper.io.yml b/config/locales/doorkeeper.io.yml
index 0384d968be..94efbfc9d7 100644
--- a/config/locales/doorkeeper.io.yml
+++ b/config/locales/doorkeeper.io.yml
@@ -60,6 +60,7 @@ io:
       error:
         title: Eroro eventis
       new:
+        prompt_html: "%{client_name} volas permiso por adirar vua konto."
         review_permissions: Kontrolez permisi
         title: Yurizo bezonesas
       show:
@@ -82,6 +83,7 @@ io:
         access_denied: Moyenproprietanto o yurizservilo refuzis la demando.
         credential_flow_not_configured: Moyenproprietantpasvortidentesesofluo faliis pro ke Doorkeeper.configure.resource_owner_from_credentials ne ajustesis.
         invalid_client: Klientpermiso falias pro nesavita kliento, neinkluzita klientpermiso o nesuportita permismetodo.
+        invalid_code_challenge_method: La kodexchalenjmetodo mustas esar S256.
         invalid_grant: Provizita yurizo esis nevalida, expiris, deaprobesis, ne parigas uzita ridirektoligilo dum yurizdemando o facesis a altra kliento.
         invalid_redirect_uri: La inkluzita ridirektoligilo esas nevalida.
         invalid_request:
@@ -123,7 +125,7 @@ io:
         admin/reports: Administro di raporti
         all: Kompleta aceso a vua Mastodon-konto
         blocks: Restriktita
-        bookmarks: Libromarki
+        bookmarks: Lektosigni
         conversations: Konversi
         crypto: Intersequanta chifro
         favourites: Favoriziti
@@ -131,9 +133,10 @@ io:
         follow: Sequati, silencigati e blokusati
         follows: Sequati
         lists: Listi
-        media: Mediatachaji
+        media: Audvidajaddonaji
         mutes: Silencigati
         notifications: Avizi
+        profile: Vua Mastodon-profilo
         push: Pulsavizi
         reports: Raporti
         search: Trovez
@@ -148,17 +151,28 @@ io:
     scopes:
       admin:read: lektez omna informi di la servilo
       admin:read:accounts: lektez privata informo di omna konti
+      admin:read:canonical_email_blocks: lektar trublema informo di omna retpostoblokusi
+      admin:read:domain_allows: lektar trublema informo di omna domenpermisi
+      admin:read:domain_blocks: lektar trublema informo di omna domenblokusi
+      admin:read:email_domain_blocks: lektar trublema informo di omna retpostodomenblokusi
+      admin:read:ip_blocks: lektar trublema informo di omna IP-blokusi
       admin:read:reports: lektez privata informo di omna raporti e raportizita konti
-      admin:write: modifikez omna informi di la servilo
+      admin:write: redaktar omna informi di la servilo
       admin:write:accounts: jerez konti
+      admin:write:canonical_email_blocks: agar jeri ad retpostoblokusi
+      admin:write:domain_allows: agar jeri ad domenpermisi
+      admin:write:domain_blocks: agar jeri ad domenblokusi
+      admin:write:email_domain_blocks: agar jeri ad retpostodomenblokusi
+      admin:write:ip_blocks: agar jeri ad IP-blokusi
       admin:write:reports: jerez raporti
       crypto: uzas intersequanta chifro
       follow: follow, block, unblock and unfollow accounts
+      profile: lektar nur profilinformo di vua konto
       push: ganez vua pulsavizi
       read: read your account's data
       read:accounts: videz kontinformo
       read:blocks: videz restrikti
-      read:bookmarks: videz vua libromarki
+      read:bookmarks: vidar vua lektosigni
       read:favourites: videz vua favoriziti
       read:filters: videz vua filtrili
       read:follows: videz vua sequinti
@@ -169,15 +183,15 @@ io:
       read:search: trovez por vu
       read:statuses: videz omna posti
       write: post on your behalf
-      write:accounts: modifikez vua porfilo
+      write:accounts: redaktar vua porfilo
       write:blocks: restriktez konti e domeni
-      write:bookmarks: libromarkez posti
+      write:bookmarks: lektosignar afishi
       write:conversations: silencigez e efacez konversi
       write:favourites: favorizita posti
       write:filters: kreez filtrili
       write:follows: sequez personi
       write:lists: kreez listi
-      write:media: adchargez mediifaili
+      write:media: adkargar audvidajdoseri
       write:mutes: silencigez personi e konversi
       write:notifications: efacez vua avizi
       write:reports: raportizez altra omni
diff --git a/config/locales/doorkeeper.kab.yml b/config/locales/doorkeeper.kab.yml
index fa9e1c540a..c65bada409 100644
--- a/config/locales/doorkeeper.kab.yml
+++ b/config/locales/doorkeeper.kab.yml
@@ -111,9 +111,9 @@ kab:
         lists: Tibdarin
         media: Imeddayen n umidya
         mutes: Yeggugem
-        notifications: Alɣuten
+        notifications: Ilɣa
         profile: Amaɣnu-k Mastodon
-        push: Alɣuten yettudemmren
+        push: Ilɣa yettudemmren
         reports: Ineqqisen
         search: Nadi
         statuses: Tisuffaɣ
@@ -127,7 +127,7 @@ kab:
       admin:read: ad iɣeṛ akk isefka ɣef uqeddac
       admin:write: ad iẓreg akk isefka ɣef uqeddac
       follow: ad ibeddel assaɣen n umiḍan
-      push: ad iṭṭef-d alɣuten-ik·im yettwademren
+      push: ad iṭṭef-d ilɣa-k·m yettwademren
       read: ad iɣeṛ akk isefka n umiḍan-ik·im
       read:accounts: ẓer isallen n yimiḍanen
       read:blocks: ẓer imiḍanen i tesḥebseḍ
@@ -136,7 +136,7 @@ kab:
       read:follows: ẓer imeḍfaṛen-ik
       read:lists: ẓer tibdarin-ik·im
       read:mutes: ẓer wid i tesgugmeḍ
-      read:notifications: ad iẓer alɣuten-inek·inem
+      read:notifications: ad iẓer ilɣa-inek·inem
       read:reports: ẓer ineqqisen-ik·im
       read:search: anadi deg umkan-ik·im
       read:statuses: ad iẓer meṛṛa tisuffaɣ
@@ -148,4 +148,4 @@ kab:
       write:follows: ḍfeṛ imdanen
       write:lists: ad yesnulfu tibdarin
       write:media: ad yessali ifuyla n umidya
-      write:notifications: sfeḍ alɣuten-ik·im
+      write:notifications: sfeḍ ilɣa-k·m
diff --git a/config/locales/doorkeeper.lv.yml b/config/locales/doorkeeper.lv.yml
index af892d79fa..15c1e7692a 100644
--- a/config/locales/doorkeeper.lv.yml
+++ b/config/locales/doorkeeper.lv.yml
@@ -150,13 +150,13 @@ lv:
         title: OAuth nepieciešama autorizācija
     scopes:
       admin:read: lasīt visus datus uz servera
-      admin:read:accounts: lasīt sensitīvu informāciju no visiem kontiem
-      admin:read:canonical_email_blocks: lasīt sensitīvu informāciju par visiem kanoniskajiem e-pasta blokiem
-      admin:read:domain_allows: lasīt visu domēnu sensitīvo informāciju, ko atļauj
-      admin:read:domain_blocks: lasīt sensitīvu informāciju par visiem domēna blokiem
-      admin:read:email_domain_blocks: lasīt sensitīvu informāciju par visiem e-pasta domēna blokiem
-      admin:read:ip_blocks: lasīt sensitīvu informāciju par visiem IP blokiem
-      admin:read:reports: lasīt sensitīvu informāciju no visiem pārskatiem un kontiem, par kuriem ziņots
+      admin:read:accounts: lasīt jūtīgu informāciju no visiem kontiem
+      admin:read:canonical_email_blocks: lasīt jūtīgu informāciju par visiem kanoniskajiem e-pasta blokiem
+      admin:read:domain_allows: lasīt jūtīgu informāciju par visiem atļautajiem domēniem
+      admin:read:domain_blocks: lasīt jūtīgu informāciju par visiem domēna blokiem
+      admin:read:email_domain_blocks: lasīt jūtīgu informāciju par visiem e-pasta domēna blokiem
+      admin:read:ip_blocks: lasīt jūtīgu informāciju par visiem IP blokiem
+      admin:read:reports: lasīt jūtīgu informāciju no visiem pārskatiem un kontiem, par kuriem ziņots
       admin:write: modificēt visus datus uz servera
       admin:write:accounts: veikt satura pārraudzības darbības kontos
       admin:write:canonical_email_blocks: veikt satura pārraudzības darbības kanoniskajos e-pasta blokos
diff --git a/config/locales/doorkeeper.ms.yml b/config/locales/doorkeeper.ms.yml
index b52824e8c3..aadce76efd 100644
--- a/config/locales/doorkeeper.ms.yml
+++ b/config/locales/doorkeeper.ms.yml
@@ -126,13 +126,13 @@ ms:
         bookmarks: Penanda buku
         conversations: Perbualan
         crypto: Penyulitan hujung ke hujung
-        favourites: Kegemaran
+        favourites: Sukaan
         filters: Penapis
-        follow: Ikut, Senyap dan Blok
+        follow: Ikutan, Redaman dan Sekatan
         follows: Ikutan
         lists: Senarai
         media: Lampiran media
-        mutes: Senyapkan
+        mutes: Redaman
         notifications: Pemberitahuan
         push: Pemberitahuan segera
         reports: Laporan
@@ -169,11 +169,11 @@ ms:
       read:accounts: lihat maklumat akaun
       read:blocks: lihat blok anda
       read:bookmarks: lihat penanda halaman anda
-      read:favourites: lihat kegemaran anda
+      read:favourites: lihat sukaan anda
       read:filters: lihat penapis anda
       read:follows: lihat senarai yang anda ikuti
       read:lists: lihat senarai anda
-      read:mutes: lihat senarai yang anda senyapkan
+      read:mutes: lihat redamanku
       read:notifications: lihat notifikasi anda
       read:reports: lihat laporan anda
       read:search: cari bagi pihak anda
@@ -182,13 +182,13 @@ ms:
       write:accounts: ubaisuai profail anda
       write:blocks: domain dan akaun blok
       write:bookmarks: menandabuku hantaran
-      write:conversations: senyapkan dan padamkan perbualan
-      write:favourites: pos kesukaan
+      write:conversations: redamkan dan padamkan perbualan
+      write:favourites: hantaran disukai
       write:filters: cipta penapis
       write:follows: ikut orang
       write:lists: cipta senarai
       write:media: memuat naik fail media
-      write:mutes: membisukan orang dan perbualan
+      write:mutes: redamkan orang dan perbualan
       write:notifications: kosongkan pemberitahuan anda
       write:reports: melaporkan orang lain
       write:statuses: terbitkan hantaran
diff --git a/config/locales/doorkeeper.pt-BR.yml b/config/locales/doorkeeper.pt-BR.yml
index 85bf5d60c0..a92819bf68 100644
--- a/config/locales/doorkeeper.pt-BR.yml
+++ b/config/locales/doorkeeper.pt-BR.yml
@@ -61,7 +61,7 @@ pt-BR:
         title: Ocorreu um erro
       new:
         prompt_html: "%{client_name} gostaria de permissão para acessar sua conta. <strong>Aprove esta solicitação apenas se você reconhecer e confiar nesta fonte.</strong>"
-        review_permissions: Rever permissões
+        review_permissions: Revisar permissões
         title: Autorização necessária
       show:
         title: Copie este código de autorização e cole no aplicativo.
@@ -130,7 +130,7 @@ pt-BR:
         crypto: Criptografia de ponta a ponta
         favourites: Favoritos
         filters: Filtros
-        follow: Seguimentos, Silenciamentos e Bloqueios
+        follow: Seguidos, Silenciados e Bloqueados
         follows: Seguidos
         lists: Listas
         media: Mídias anexadas
@@ -165,7 +165,7 @@ pt-BR:
       admin:write:email_domain_blocks: executar ações de moderação em domínios de e-mail bloqueados
       admin:write:ip_blocks: executar ações de moderação em IPs bloqueados
       admin:write:reports: executar ações de moderação em denúncias
-      crypto: usar criptografia de ponta-a-ponta
+      crypto: usar criptografia de ponta a ponta
       follow: alterar o relacionamento das contas
       profile: ler somente as informações do perfil da sua conta
       push: receber notificações push
diff --git a/config/locales/doorkeeper.sl.yml b/config/locales/doorkeeper.sl.yml
index 3f36c73756..8b28d1532a 100644
--- a/config/locales/doorkeeper.sl.yml
+++ b/config/locales/doorkeeper.sl.yml
@@ -60,6 +60,7 @@ sl:
       error:
         title: Prišlo je do napake
       new:
+        prompt_html: "%{client_name} želi dostopati do vašega računa. <strong>To prošnjo odobrite le, če tega odjemalca prepoznate in mu zaupate.</strong>"
         review_permissions: Preglej dovoljenja
         title: Potrebna je odobritev
       show:
@@ -82,6 +83,7 @@ sl:
         access_denied: Lastnik virov ali odobritveni strežnik je zavrnil zahtevo.
         credential_flow_not_configured: Pretok geselskih pooblastil lastnika virov ni uspel, ker Doorkeeper.configure.resource_owner_from_credentials ni nastavljen.
         invalid_client: Odobritev odjemalca ni uspela zaradi neznanega odjemalca, zaradi nevključitve odobritve odjemalca ali zaradi nepodprte metode odobritve.
+        invalid_code_challenge_method: Metoda za kodo mora biti S256, čistopis ni podprt.
         invalid_grant: Predložena odobritev je neveljavna, je potekla, je preklicana, se ne ujema z URI-jem za preusmeritev uporabljenim v zahtevi za odobritev, ali pa je bila izdana drugemu odjemalcu.
         invalid_redirect_uri: URI za preusmeritev ni veljaven.
         invalid_request:
diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml
index 256ecbb776..2c096720c2 100644
--- a/config/locales/doorkeeper.zh-CN.yml
+++ b/config/locales/doorkeeper.zh-CN.yml
@@ -185,7 +185,7 @@ zh-CN:
       write: 修改你的账号数据
       write:accounts: 修改你的个人资料
       write:blocks: 屏蔽账号与站点
-      write:bookmarks: 收藏嘟文
+      write:bookmarks: 将嘟文加入书签
       write:conversations: 静音并删除会话
       write:favourites: 喜欢嘟文
       write:filters: 创建过滤规则
diff --git a/config/locales/el.yml b/config/locales/el.yml
index 0c40ffabdc..d2f4fbba01 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -309,6 +309,7 @@ el:
       title: Αρχείο ελέγχου
       unavailable_instance: "(μη διαθέσιμο όνομα τομέα)"
     announcements:
+      back: Επιστροφή στις ανακοινώσεις
       destroyed_msg: Επιτυχής διαγραφή ανακοίνωσης!
       edit:
         title: Ενημέρωση ανακοίνωσης
@@ -317,6 +318,9 @@ el:
       new:
         create: Δημιουργία ανακοίνωσης
         title: Νέα ανακοίνωση
+      preview:
+        explanation_html: 'Το email θα αποσταλεί σε <strong>%{display_count} χρήστες</strong>. Το ακόλουθο κείμενο θα συμπεριληφθεί στο e-mail:'
+        title: Προεπισκόπηση ειδοποίησης ανακοίνωσης
       publish: Δημοσίευση
       published_msg: Επιτυχής δημοσίευση ανακοίνωσης!
       scheduled_for: Προγραμματισμένη για %{time}
@@ -475,6 +479,34 @@ el:
       new:
         title: Εισαγωγή αποκλεισμένων τομέων
       no_file: Δεν επιλέχθηκε αρχείο
+    fasp:
+      debug:
+        callbacks:
+          created_at: Δημιουργήθηκε στις
+          delete: Διαγραφή
+          ip: Διεύθυνση IP
+          request_body: Σώμα αιτήματος
+          title: Κλήσεις Αποσφαλμάτωσης
+      providers:
+        active: Ενεργό
+        base_url: URL βάσης
+        callback: Επανάκληση
+        delete: Διαγραφή
+        edit: Επεξεργασία Παρόχου
+        finish_registration: Ολοκλήρωση εγγραφής
+        name: Όνομα
+        providers: Πάροχοι
+        public_key_fingerprint: Αποτύπωμα δημόσιου κλειδιού
+        registration_requested: Η εγγραφή ζητήθηκε
+        registrations:
+          confirm: Επιβεβαίωση
+          description: Έλαβες μια εγγραφή από ένα FASP. Απέρριψέ την αν δεν την άρχισες εσύ. Αν το άρχισες εσύ, σύγκρινε προσεκτικά το όνομα και το κλειδί αποτύπωμα πριν από την επιβεβαίωση της εγγραφής.
+          reject: Απόρριψη
+          title: Επιβεβαίωση Εγγραφής FASP
+        save: Αποθήκευση
+        select_capabilities: Επέλεξε Δυνατότητες
+        sign_in: Σύνδεση
+        status: Κατάσταση
     follow_recommendations:
       description_html: "<strong>Ακολουθώντας συστάσεις βοηθάει τους νέους χρήστες να βρουν γρήγορα ενδιαφέρον περιεχόμενο</strong>. Όταν ένας χρήστης δεν έχει αλληλεπιδράσει με άλλους αρκετά για να διαμορφώσει εξατομικευμένες συστάσεις, συνιστώνται αυτοί οι λογαριασμοί. Υπολογίζονται εκ νέου σε καθημερινή βάση από ένα σύνολο λογαριασμών με τις υψηλότερες πρόσφατες αλληλεπιδράσεις και μεγαλύτερο αριθμό τοπικών ακόλουθων για μια δεδομένη γλώσσα."
       language: Για τη γλώσσα
@@ -1936,8 +1968,6 @@ el:
     terms_of_service_changed:
       agreement: Συνεχίζοντας να χρησιμοποιείς το %{domain}, συμφωνείς με αυτούς τους όρους. Αν διαφωνείς με τους ενημερωμένους όρους, μπορείς να τερματίσεις τη συμφωνία σου με το %{domain} ανά πάσα στιγμή διαγράφοντας τον λογαριασμό σου.
       changelog: 'Με μια ματιά, αυτό σημαίνει αυτή η ενημέρωση για σένα:'
-      description: 'Λαμβάνεις αυτό το μήνυμα επειδή κάνουμε κάποιες αλλαγές στους όρους παροχής υπηρεσιών μας στο %{domain}. Σε ενθαρρύνουμε να εξετάσεις πλήρως τους ενημερωμένους όρους εδώ:'
-      description_html: Λαμβάνεις αυτό το μήνυμα επειδή κάνουμε κάποιες αλλαγές στους όρους παροχής υπηρεσιών μας στο %{domain}. Σε ενθαρρύνουμε να εξετάσεις <a href="%{path}" target="_blank">πλήρως τους ενημερωμένους όρους εδώ</a>.
       sign_off: Η ομάδα του %{domain}
       subject: Ενημερώσεις στους όρους παροχής υπηρεσιών μας
       subtitle: Οι όροι παροχής υπηρεσιών του %{domain} αλλάζουν
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml
index c8c3e486b7..92934f50f0 100644
--- a/config/locales/en-GB.yml
+++ b/config/locales/en-GB.yml
@@ -1936,8 +1936,6 @@ en-GB:
     terms_of_service_changed:
       agreement: By continuing to use %{domain}, you are agreeing to these terms. If you disagree with the updated terms, you may terminate your agreement with %{domain} at any time by deleting your account.
       changelog: 'At a glance, here is what this update means for you:'
-      description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. We encourage you to review the updated terms in full here:'
-      description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. We encourage you to review the <a href="%{path}" target="_blank">updated terms in full here</a>.
       sign_off: The %{domain} team
       subject: Updates to our terms of service
       subtitle: The terms of service of %{domain} are changing
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 98bee0bd2b..dca6a2d9f8 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -325,6 +325,7 @@ en:
       title: Audit log
       unavailable_instance: "(domain name unavailable)"
     announcements:
+      back: Back to announcements
       destroyed_msg: Announcement successfully deleted!
       edit:
         title: Edit announcement
@@ -333,6 +334,10 @@ en:
       new:
         create: Create announcement
         title: New announcement
+      preview:
+        disclaimer: As users cannot opt out of them, email notifications should be limited to important announcements such as personal data breach or server closure notifications.
+        explanation_html: 'The email will be sent to <strong>%{display_count} users</strong>. The following text will be included in the e-mail:'
+        title: Preview announcement notification
       publish: Publish
       published_msg: Announcement successfully published!
       scheduled_for: Scheduled for %{time}
@@ -525,6 +530,36 @@ en:
       new:
         title: Import domain blocks
       no_file: No file selected
+    fasp:
+      debug:
+        callbacks:
+          created_at: Created at
+          delete: Delete
+          ip: IP address
+          request_body: Request body
+          title: Debug Callbacks
+      providers:
+        active: Active
+        base_url: Base URL
+        callback: Callback
+        delete: Delete
+        edit: Edit Provider
+        finish_registration: Finish registration
+        name: Name
+        providers: Providers
+        public_key_fingerprint: Public key fingerprint
+        registration_requested: Registration requested
+        registrations:
+          confirm: Confirm
+          description: You received a registration from a FASP. Reject it if you did not initiate this. If you initiated this, carefully compare name and key fingerprint before confirming the registration.
+          reject: Reject
+          title: Confirm FASP Registration
+        save: Save
+        select_capabilities: Select Capabilities
+        sign_in: Sign In
+        status: Status
+        title: Fediverse Auxiliary Service Providers
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Follow recommendations help new users quickly find interesting content</strong>. When a user has not interacted with others enough to form personalized follow recommendations, these accounts are recommended instead. They are re-calculated on a daily basis from a mix of accounts with the highest recent engagements and highest local follower counts for a given language."
       language: For language
@@ -1203,6 +1238,7 @@ en:
         chance_to_review_html: "<strong>The generated terms of service will not be published automatically.</strong> You will have a chance to review the results. Please fill in the necessary details to proceed."
         explanation_html: The terms of service template provided is for informational purposes only, and should not be construed as legal advice on any subject matter. Please consult with your own legal counsel on your situation and specific legal questions you have.
         title: Terms of Service Setup
+      going_live_on_html: Live, effective %{date}
       history: History
       live: Live
       no_history: There are no recorded changes of the terms of service yet.
@@ -2274,6 +2310,10 @@ en:
     recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
     webauthn: Security keys
   user_mailer:
+    announcement_published:
+      description: 'The administrators of %{domain} are making an announcement:'
+      subject: Service announcement
+      title: "%{domain} service announcement"
     appeal_approved:
       action: Account Settings
       explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been approved. Your account is once again in good standing.
@@ -2306,8 +2346,8 @@ en:
     terms_of_service_changed:
       agreement: By continuing to use %{domain}, you are agreeing to these terms. If you disagree with the updated terms, you may terminate your agreement with %{domain} at any time by deleting your account.
       changelog: 'At a glance, here is what this update means for you:'
-      description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. We encourage you to review the updated terms in full here:'
-      description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. We encourage you to review the <a href="%{path}" target="_blank">updated terms in full here</a>.
+      description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. These updates will take effect on %{date}. We encourage you to review the updated terms in full here:'
+      description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. These updates will take effect on <strong>%{date}</strong>. We encourage you to review the <a href="%{path}" target="_blank">updated terms in full here</a>.
       sign_off: The %{domain} team
       subject: Updates to our terms of service
       subtitle: The terms of service of %{domain} are changing
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index d56d67e28e..a514d332a8 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -5,7 +5,7 @@ eo:
     contact_missing: Ne elektita
     contact_unavailable: Ne disponebla
     hosted_on: "%{domain} estas nodo de Mastodon"
-    title: Pri
+    title: Prio
   accounts:
     followers:
       one: Sekvanto
@@ -41,7 +41,7 @@ eo:
       by_domain: Domajno
       change_email:
         changed_msg: Retpoŝta adreso sukcese ŝanĝita!
-        current_email: Nuna retadreso
+        current_email: Aktuala retadreso
         label: Ŝanĝi retadreson
         new_email: Nova retadreso
         submit: Ŝanĝi retadreson
@@ -102,7 +102,7 @@ eo:
       moderation_notes: Notoj de moderigado
       most_recent_activity: Lastaj afiŝoj
       most_recent_ip: Lasta IP
-      no_account_selected: Neniu konto estis ŝanĝita ĉar neniu estis selektita
+      no_account_selected: Neniu konto estis ŝanĝita kial neniu estis selektita
       no_limits_imposed: Neniu limito trudita
       no_role_assigned: Sen rolo
       not_subscribed: Ne abonita
@@ -142,7 +142,7 @@ eo:
         only_password: Nur pasvorto
         password_and_2fa: Pasvorto kaj 2FA
       sensitive: Tikla
-      sensitized: markita tikla
+      sensitized: Markita kiel tikla
       shared_inbox_url: URL de kunhavigita leterkesto
       show:
         created_reports: Faritaj raportoj
@@ -204,7 +204,7 @@ eo:
         destroy_unavailable_domain: Forigi Nehaveblan Domajnon
         destroy_user_role: Detrui Rolon
         disable_2fa_user: Malebligi 2FA
-        disable_custom_emoji: Malebligi proprajn emoĝiojn
+        disable_custom_emoji: Malŝalti Proprajn Bildosignojn
         disable_relay: Malebligi Relajson
         disable_sign_in_token_auth_user: Malebligi aŭtentigon per retpoŝta ĵetono por la uzanto
         disable_user: Neebligi la uzanton
@@ -220,14 +220,14 @@ eo:
         remove_avatar_user: Forigi la profilbildon
         reopen_report: Remalfermi signalon
         resend_user: Resendi konfirman retmesaĝon
-        reset_password_user: Restarigi pasvorton
+        reset_password_user: Restarigi Pasvorton
         resolve_report: Solvitaj reporto
         sensitive_account: Marki tikla la aŭdovidaĵojn de via konto
-        silence_account: Silentigi konton
-        suspend_account: Suspendi la konton
+        silence_account: Silentigi Konton
+        suspend_account: Suspendi Konton
         unassigned_report: Malatribui Raporton
         unblock_email_account: Malbloki retpoŝtadreson
-        unsensitive_account: Malmarku la amaskomunikilojn en via konto kiel sentemaj
+        unsensitive_account: Malmarki Konton kiel Sentime Tikla
         unsilence_account: Malsilentigi konton
         unsuspend_account: Malsuspendi la konton
         update_announcement: Ĝisdatigi anoncon
@@ -235,19 +235,19 @@ eo:
         update_domain_block: Ĝigdatigi domajnan blokadon
         update_ip_block: Krei IP-regulon
         update_report: Ĝisdatigo de Raporto
-        update_status: Ĝisdatigi afiŝon
+        update_status: Ĝisdatigi Afiŝon
         update_user_role: Ĝisdatigi rolon
       actions:
-        approve_appeal_html: "%{name} aprobis apelacion kontraŭ moderiga decido de %{target}"
-        approve_user_html: "%{name} aprobis registriĝon de %{target}"
+        approve_appeal_html: "%{name} aprobis apelacion kontraŭ reguliga decido de %{target}"
+        approve_user_html: "%{name} aprobis la registriĝon de %{target}"
         assigned_to_self_report_html: "%{name} asignis signalon %{target} al si mem"
-        change_email_user_html: "%{name} ŝanĝis retadreson de uzanto %{target}"
+        change_email_user_html: "%{name} ŝanĝis la retadreson de uzanto %{target}"
         change_role_user_html: "%{name} ŝanĝis rolon de %{target}"
         confirm_user_html: "%{name} konfirmis retadreson de uzanto %{target}"
         create_account_warning_html: "%{name} sendis averton al %{target}"
         create_announcement_html: "%{name} kreis novan anoncon %{target}"
         create_canonical_email_block_html: "%{name} blokis retpoŝtadreson per krado %{target}"
-        create_custom_emoji_html: "%{name} alŝutis novan emoĝion %{target}"
+        create_custom_emoji_html: "%{name} alŝutis novan bildosignon %{target}"
         create_domain_allow_html: "%{name} aldonis domajnon %{target} al la blanka listo"
         create_domain_block_html: "%{name} blokis domajnon %{target}"
         create_email_domain_block_html: "%{name} blokis retpoŝtan domajnon %{target}"
@@ -258,7 +258,7 @@ eo:
         demote_user_html: "%{name} degradis uzanton %{target}"
         destroy_announcement_html: "%{name} forigis anoncon %{target}"
         destroy_canonical_email_block_html: "%{name} malblokis retpoŝtadreson per krado %{target}"
-        destroy_custom_emoji_html: "%{name} forigis emoĝion %{target}"
+        destroy_custom_emoji_html: "%{name} forigis la bildosignon %{target}"
         destroy_domain_allow_html: "%{name} forigis domajnon %{target} el la blanka listo"
         destroy_domain_block_html: "%{name} malblokis domajnon %{target}"
         destroy_email_domain_block_html: "%{name} malblokis retpoŝtan domajnon %{target}"
@@ -269,11 +269,11 @@ eo:
         destroy_unavailable_domain_html: "%{name} restartigis sendon al domajno %{target}"
         destroy_user_role_html: "%{name} forigis rolon de %{target}"
         disable_2fa_user_html: "%{name} malebligis dufaktoran aŭtentigon por uzanto %{target}"
-        disable_custom_emoji_html: "%{name} malebligis la emoĝion %{target}"
+        disable_custom_emoji_html: "%{name} malebligis la bildosignon %{target}"
         disable_relay_html: "%{name} malebligis la relajson %{target}"
         disable_sign_in_token_auth_user_html: "%{name} malebligis la aŭtentigon de retpoŝta ĵetono por %{target}"
         disable_user_html: "%{name} malebligis ensaluton por uzanto %{target}"
-        enable_custom_emoji_html: "%{name} ebligis la emoĝion %{target}"
+        enable_custom_emoji_html: "%{name} ebligis la bildosignon %{target}"
         enable_relay_html: "%{name} ebligis la relajson %{target}"
         enable_sign_in_token_auth_user_html: "%{name} ebligis la aŭtentigon de retpoŝta ĵetono por %{target}"
         enable_user_html: "%{name} ebligis ensaluton por uzanto %{target}"
@@ -296,7 +296,7 @@ eo:
         unsilence_account_html: "%{name} malfaris limon al konto de %{target}"
         unsuspend_account_html: "%{name} malsuspendis la konton de %{target}"
         update_announcement_html: "%{name} ĝisdatigis la anoncon %{target}"
-        update_custom_emoji_html: "%{name} ĝisdatigis la emoĝion %{target}"
+        update_custom_emoji_html: "%{name} ĝisdatigis la bildosignon %{target}"
         update_domain_block_html: "%{name} ĝisdatigis domajnblokon por %{target}"
         update_ip_block_html: "%{name} ŝanĝis regulon por IP %{target}"
         update_report_html: "%{name} ĝisdatigis la raporton %{target}"
@@ -309,17 +309,21 @@ eo:
       title: Ĵurnalo de revizo
       unavailable_instance: "(domajna nomo nedisponebla)"
     announcements:
-      destroyed_msg: Anonco sukcese forigita!
+      back: Reen al anoncoj
+      destroyed_msg: La anonco sukcese forigita!
       edit:
         title: Redakti anoncon
-      empty: Neniu anonco trovita.
+      empty: Neniu anonco troviĝas.
       live: Publikigita
       new:
         create: Krei anoncon
         title: Nova anonco
+      preview:
+        explanation_html: 'La retpoŝto estos sendita al <strong>%{display_count} uzantoj</strong>. La jena teksto estos inkluzivita en la retmesaĝon:'
+        title: Antaŭrigardu sciigan anoncon
       publish: Publikigi
       published_msg: Anonco sukcese publikigita!
-      scheduled_for: Planigita je %{time}
+      scheduled_for: Planigota je %{time}
       scheduled_msg: Anonco planigita por publikigo!
       title: Anoncoj
       unpublish: Malpublikigi
@@ -341,7 +345,7 @@ eo:
       disabled_msg: Emoĝio sukcese malebligita
       emoji: Emoĝio
       enable: Ebligi
-      enabled: Ebligita
+      enabled: Ŝaltita
       enabled_msg: Emoĝio sukcese ebligita
       image_hint: PNG aŭ GIF malpli granda ol %{size}
       list: Listo
@@ -363,7 +367,7 @@ eo:
     dashboard:
       active_users: aktivaj uzantoj
       interactions: interago
-      media_storage: Konservo de plurmedioj
+      media_storage: Konservo de aŭdovidaĵoj
       new_users: novaj uzantoj
       opened_reports: raportoj malfermitaj
       pending_appeals_html:
@@ -475,6 +479,16 @@ eo:
       new:
         title: Importi domajnblokojn
       no_file: Neniu dosiero elektita
+    fasp:
+      debug:
+        callbacks:
+          delete: Forigi
+      providers:
+        delete: Forigi
+        name: Nomo
+        registrations:
+          confirm: Konfirmi
+        sign_in: Ensaluti
     follow_recommendations:
       description_html: "<strong>Sekvorekomendoj helpi novajn uzantojn rapide trovi interesa enhavo</strong>. Ili rekalkulitas ĉiutage lau interagoj kaj sekvantokvantoj."
       language: Por la lingvo
@@ -508,7 +522,7 @@ eo:
         description_html: Vi povas difini enhavopolitikojn al la ĉiuj kontoj.
         limited_federation_mode_description_html: Vi povas elekti, ĉu permesi federacion kun tiu domajno.
         policies:
-          reject_media: Malakcepti plurmediojn
+          reject_media: Malakcepti la vidaŭdaĵojn
           reject_reports: Malakcepti raportojn
           silence: Kaŝu
           suspend: Suspendi
@@ -939,6 +953,7 @@ eo:
         chance_to_review_html: "<strong>La generataj kondiĉoj de uzado ne aŭtomate publikiĝos.</strong> Estos oportuni por vi kontroli la rezultojn. Bonvole entajpu la necesajn detalojn por daŭrigi."
         explanation_html: La modelo por la kondiĉoj de la servo disponeblas sole por informi. Ĝi nepre ne estas leĝa konsilo pri iu ajn temo. Bonvole konsultu vian propran leĝan konsilanton pri via situacio kaj iuj leĝaj neklarecoj.
         title: Agordo de kondiĉoj de uzado
+      going_live_on_html: Efektiviĝos %{date}
       history: Historio
       live: Antaŭmontro
       no_history: Ankoraŭ ne estas registritaj ŝanĝoj de la kondiĉoj de la servo.
@@ -1313,8 +1328,8 @@ eo:
       Por uzi la retan aplikaĵon de Mastodon, bonvolu ebligi JavaScript. Alimaniere, provu unu el la
       <a href="%{apps_path}">operaciumaj aplikaĵoj</a> por Mastodon por via platformo.
   existing_username_validator:
-    not_found: Ne povas trovi lokaj uzanto kun tiu uzantnomo
-    not_found_multiple: Ne povas trovi %{usernames}
+    not_found: ne povas trovi lokaj uzanto kun tiu uzantnomo
+    not_found_multiple: ne povas trovi %{usernames}
   exports:
     archive_takeout:
       date: Dato
@@ -1539,7 +1554,7 @@ eo:
     validations:
       images_and_video: Aldoni videon al mesaĝo, kiu jam havas bildojn ne eblas
       not_found: Dosiero %{ids} ne trovis aŭ jam alteniĝis al alia afiŝo
-      not_ready: Ne povas aldoni dosieron kiu ne finas procezitis.
+      not_ready: Ne povas aldoni dosieron kiu ne finis procezitis.
       too_many: Aldoni pli ol 4 dosierojn ne eblas
   migrations:
     acct: Movigita al
@@ -1777,7 +1792,7 @@ eo:
     migrate: Konta migrado
     notifications: Retpoŝtaj sciigoj
     preferences: Preferoj
-    profile: Profilo
+    profile: Publika profilo
     relationships: Sekvatoj kaj sekvantoj
     severed_relationships: Finitaj rilatoj
     statuses_cleanup: Automata mesaĝforigo
@@ -1906,6 +1921,10 @@ eo:
     recovery_instructions_html: Se vi perdas aliron al via telefono, vi povas uzi unu el la subaj realiraj kodoj por rehavi aliron al via konto. <strong>Konservu realirajn kodojn sekure</strong>. Ekzemple, vi povas printi ilin kaj konservi ilin kun aliaj gravaj dokumentoj.
     webauthn: Sekurecaj ŝlosiloj
   user_mailer:
+    announcement_published:
+      description: 'La administrantoj de %{domain} faras anoncon:'
+      subject: Serva anonco
+      title: "%{domain} serva anonco"
     appeal_approved:
       action: Konto-agordoj
       explanation: La apelacio de la admono kontra via konto je %{strike_date} pri sendodato %{appeal_date} aprobitas.
@@ -1938,8 +1957,8 @@ eo:
     terms_of_service_changed:
       agreement: Se vi daŭrige uzos %{domain}, vi aŭtomate interkonsentos pri ĉi tiuj kondiĉoj. Se vi malkonsentas pri la novaj kondiĉoj, vi ĉiutempe rajtas nuligi la interkonsenton kun %{domain} per forigi vian konton.
       changelog: 'Facile dirite, la ŝanĝoj estas la jenaj:'
-      description: 'Vi ricevas ĉi tiun retmesaĝon ĉar ni faras iujn ŝanĝojn al niaj servokondiĉoj ĉe %{domain}. Ni instigas vin revizii la ĝisdatigitajn kondiĉojn tute ĉi tie:'
-      description_html: Vi ricevas ĉi tiun retmesaĝon ĉar ni faras iujn ŝanĝojn al niaj servokondiĉoj ĉe %{domain}. Ni instigas vin revizii la <a href="%{path}" target="_blank">ĝisdatigitajn kondiĉojn plene ĉi tie</a>.
+      description: 'Vi ricevas ĉi tiun retmesaĝon ĉar ni faras iujn ŝanĝojn al niaj servokondiĉoj ĉe %{domain}. Ĉi tiuj ĝisdatigoj efektiviĝos %{date}. Ni instigas vin revizii la ĝisdatigitajn terminojn plene ĉi tie:'
+      description_html: Vi ricevas ĉi tiun retmesaĝon ĉar ni faras iujn ŝanĝojn al niaj servokondiĉoj ĉe %{domain}. Ĉi tiuj ĝisdatigoj efektiviĝos <strong>%{date}</strong> Ni instigas vin revizii la <a href="%{path}" target="_blank">ĝisdatigitajn terminojn plene ĉi tie</a>.
       sign_off: La teamo de %{domain}
       subject: Ĝisdatigoj al niaj kondiĉoj de servo
       subtitle: La kondiĉoj de la servo de %{domain} ŝanĝiĝas
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index 2f313da8d4..ab3d307fd8 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -309,6 +309,7 @@ es-AR:
       title: Registro de auditoría
       unavailable_instance: "[nombre de dominio no disponible]"
     announcements:
+      back: Volver a los anuncios
       destroyed_msg: "¡Anuncio eliminado exitosamente!"
       edit:
         title: Editar anuncio
@@ -317,6 +318,10 @@ es-AR:
       new:
         create: Crear anuncio
         title: Nuevo anuncio
+      preview:
+        disclaimer: Como los usuarios no pueden excluirse de ellas, las notificaciones por correo electrónico deberían limitarse a anuncios importantes como la violación de datos personales o las notificaciones de cierre del servidor.
+        explanation_html: 'El correo electrónico se enviará a <strong>%{display_count} usuarios</strong>. En el correo electrónico se incluirá el siguiente texto:'
+        title: Previsualizar la notificación del anuncio
       publish: Publicar
       published_msg: "¡Anuncio publicado exitosamente!"
       scheduled_for: Programado para %{time}
@@ -475,6 +480,36 @@ es-AR:
       new:
         title: Importar bloques de dominio
       no_file: No hay ningún archivo seleccionado
+    fasp:
+      debug:
+        callbacks:
+          created_at: 'Fecha de creación:'
+          delete: Eliminar
+          ip: Dirección IP
+          request_body: Cuerpo de la solicitud
+          title: Depurar llamadas
+      providers:
+        active: Activo
+        base_url: Dirección web base
+        callback: Depuraciones
+        delete: Eliminar
+        edit: Editar proveedor
+        finish_registration: Finalizar registro
+        name: Nombre
+        providers: Proveedores
+        public_key_fingerprint: Huella de clave pública
+        registration_requested: Registro solicitado
+        registrations:
+          confirm: Confirmar
+          description: Recibiste un registro de un FASP (Fediverse Auxiliary Service Providers; Proveedores de Servicio Auxiliares del Fediverso). Rechazalo si no iniciaste esto. Si, en cambio, iniciaste esto, compará cuidadosamente el nombre y la huella de la clave antes de confirmar el registro.
+          reject: Rechazar
+          title: Confirmar registro FASP
+        save: Guardar
+        select_capabilities: Seleccionar capacidades
+        sign_in: Iniciar sesión
+        status: Estado
+        title: Proveedores de Servicios Auxiliares del Fediverso
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Las recomendaciones de cuentas para seguir ayudan a los nuevos usuarios a encontrar rápidamente contenido interesante</strong>. Cuando un usuario no ha interactuado con otros lo suficiente como para formar recomendaciones personalizadas de seguimiento, se recomiendan estas cuentas, en su lugar. Se recalculan diariamente a partir de una mezcla de cuentas con las interacciones más recientes y el mayor número de seguidores para un idioma determinado."
       language: Por idioma
@@ -939,6 +974,7 @@ es-AR:
         chance_to_review_html: "<strong>Los términos del servicio generados no se publicarán automáticamente.</strong> Vas a tener la oportunidad de revisar el resultado. Por favor, rellená los detalles necesarios para continuar."
         explanation_html: La plantilla de términos de servicio ofrecida es únicamente para propósito informativo, y no debería ser considerada asesoramiento legal sobre ningún tema. Por favor, consultá con tu propio asesor legal sobre tu situación y las cuestiones legales específicas que tengás.
         title: Configuración de los términos del servicio
+      going_live_on_html: Actual, efectivo desde %{date}
       history: Historial
       live: En vivo
       no_history: Aún no se han registrado cambios en los términos del servicio.
@@ -1904,6 +1940,10 @@ es-AR:
     recovery_instructions_html: Si alguna vez perdés el acceso a tu aplicación de 2FA, podés usar uno de los siguientes códigos de recuperación para recuperar el acceso a tu cuenta. <strong>Mantenelos a salvo</strong>. Por ejemplo, podés imprimirlos y guardarlos con otros documentos importantes.
     webauthn: Llaves de seguridad
   user_mailer:
+    announcement_published:
+      description: 'Los administradores de %{domain} están haciendo un anuncio:'
+      subject: Anuncio del servicio
+      title: Anuncio del servicio %{domain}
     appeal_approved:
       action: Configuración de la cuenta
       explanation: La apelación del incumplimiento contra tu cuenta del %{strike_date} que enviaste el %{appeal_date} fue aprobada. Tu cuenta se encuentra de nuevo en buen estado.
@@ -1936,8 +1976,8 @@ es-AR:
     terms_of_service_changed:
       agreement: Si seguís usando %{domain}, aceptás estos términos. Si no estás de acuerdo con los términos actualizados, podés cancelar tu acuerdo con %{domain} en cualquier momento, eliminando tu cuenta.
       changelog: 'A modo de pantallazo general, esto es lo que esta actualización significa para vos:'
-      description: 'Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos del servicio en %{domain}. Te animamos a revisar los términos actualizados en su totalidad acá:'
-      description_html: Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos del servicio en %{domain}. Te animamos a revisar los <a href="%{path}" target="_blank">términos actualizados en su totalidad acá</a>.
+      description: 'Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos de servicio en %{domain}. Estas actualizaciones serán efectivas el %{date}. Te alentamos a revisar los términos actualizados en su totalidad aquí:'
+      description_html: Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos de servicio en %{domain}. Estas actualizaciones serán efectivas el <strong>%{date}</strong>. Te alentamos a revisar los <a href="%{path}" target="_blank"> términos actualizados en su totalidad aquí</a>.
       sign_off: El equipo de %{domain}
       subject: Actualizaciones en nuestros términos del servicio
       subtitle: Los términos del servicio de %{domain} están cambiando
diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml
index 135ca6cc78..3032b663a1 100644
--- a/config/locales/es-MX.yml
+++ b/config/locales/es-MX.yml
@@ -309,6 +309,7 @@ es-MX:
       title: Log de auditoría
       unavailable_instance: "(nombre de dominio no disponible)"
     announcements:
+      back: Volver a la sección de anuncios
       destroyed_msg: "¡Anuncio eliminado con éxito!"
       edit:
         title: Editar anuncio
@@ -317,6 +318,10 @@ es-MX:
       new:
         create: Crear anuncio
         title: Nuevo anuncio
+      preview:
+        disclaimer: Como los usuarios no pueden optar por no recibirlas, las notificaciones por correo electrónico deben limitarse a anuncios importantes, como la violación de datos personales o las notificaciones de cierre de servidores.
+        explanation_html: 'El correo electrónico se enviará a <strong>%{display_count} usuarios</strong>. En el correo electrónico se incluirá el siguiente texto:'
+        title: Vista previa de la notificación del anuncio
       publish: Publicar
       published_msg: "¡Anuncio publicado con éxito!"
       scheduled_for: Programado para %{time}
@@ -475,6 +480,36 @@ es-MX:
       new:
         title: Importar bloqueos de dominio
       no_file: No hay archivo seleccionado
+    fasp:
+      debug:
+        callbacks:
+          created_at: Creado el
+          delete: Eliminar
+          ip: Dirección IP
+          request_body: Cuerpo de la solicitud
+          title: Depurar devoluciones de llamada
+      providers:
+        active: Activo
+        base_url: URL base
+        callback: Devolución de llamada
+        delete: Eliminar
+        edit: Editar proveedor
+        finish_registration: Terminar el registro
+        name: Nombre
+        providers: Proveedores
+        public_key_fingerprint: Huella digital de clave pública
+        registration_requested: Se solicitó el registro
+        registrations:
+          confirm: Confirmar
+          description: Has recibido un registro de un FASP. Recházalo si no lo iniciaste tú. Si lo comenzaste tú, compara cuidadosamente el nombre y la huella de la clave antes de confirmar el registro.
+          reject: Rechazar
+          title: Confirmar registro FASP
+        save: Guardar
+        select_capabilities: Selecciona las capacidades
+        sign_in: Iniciar sesión
+        status: Estado
+        title: Fediverse Auxiliary Service Providers (Proveedores de Servicio Auxiliares del Fediverso)
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Las recomendaciones de cuentas ayudan a los nuevos usuarios a encontrar rápidamente contenido interesante</strong>. Cuando un usuario no ha interactuado con otros lo suficiente como para suscitar recomendaciones personalizadas de cuentas a las que seguir, en su lugar se le recomiendan estas cuentas. Se recalculan diariamente a partir de una mezcla de cuentas con el mayor número de interacciones recientes y con el mayor número de seguidores locales con un idioma determinado."
       language: Para el idioma
@@ -939,6 +974,7 @@ es-MX:
         chance_to_review_html: "<strong>Las condiciones de servicio generadas no se publicarán automáticamente.</strong> Tendrás la oportunidad de revisar los resultados. Por favor, rellena los datos necesarios para continuar."
         explanation_html: La plantilla de condiciones de servicio que se proporciona tiene únicamente fines informativos y no debe interpretarse como asesoramiento jurídico sobre ningún tema. Por favor, consulte con su propio asesor legal sobre su situación y las cuestiones legales específicas que tenga.
         title: Configuración de las condiciones del servicio
+      going_live_on_html: En vigor desde %{date}
       history: Historial
       live: Actual
       no_history: No se han registrado cambios en las condiciones del servicio hasta el momento.
@@ -1904,6 +1940,10 @@ es-MX:
     recovery_instructions_html: Si pierdes acceso a tu teléfono, puedes usar uno de los siguientes códigos de recuperación para obtener acceso a tu cuenta. <strong>Mantenlos a salvo</strong>. Por ejemplo, puedes imprimirlos y guardarlos con otros documentos importantes.
     webauthn: Claves de seguridad
   user_mailer:
+    announcement_published:
+      description: 'Los administradores de %{domain} están haciendo un anuncio:'
+      subject: Anuncio
+      title: Anuncio de %{domain}
     appeal_approved:
       action: Ajustes de la cuenta
       explanation: La apelación de la amonestación contra tu cuenta del %{strike_date} que enviaste el %{appeal_date} fue aprobada. Tu cuenta se encuentra de nuevo en buen estado.
@@ -1936,8 +1976,8 @@ es-MX:
     terms_of_service_changed:
       agreement: Al seguir usando %{domain}, aceptas estas condiciones. Si no estás de acuerdo con las condiciones actualizadas, puedes cancelar tu acuerdo con %{domain} en cualquier momento eliminando tu cuenta.
       changelog: 'En pocas palabras, esto es lo que esta actualización implica para ti:'
-      description: 'Estás recibiendo este correo electrónico porque estamos realizando algunos cambios en nuestras condiciones del servicio en %{domain}. Te animamos a revisar las condiciones actualizadas en su totalidad aquí:'
-      description_html: Estás recibiendo este correo electrónico porque estamos realizando algunos cambios en nuestras condiciones del servicio en %{domain}. Te animamos a revisar las <a href="%{path}" target="_blank">condiciones actualizadas en su totalidad aquí</a>.
+      description: 'Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos de servicio en %{domain}. Estas actualizaciones entrarán en vigor en %{date}. Te animamos a revisar los términos actualizados en su totalidad aquí:'
+      description_html: Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos de servicio en %{domain}. Estas actualizaciones entrarán en vigor en <strong>%{date}</strong>. Te animamos a revisar los <a href="%{path}" target="_blank"> términos actualizados en su totalidad aquí</a>.
       sign_off: El equipo de %{domain}
       subject: Actualizaciones en nuestras condiciones del servicio
       subtitle: Las condiciones del servicio de %{domain} han cambiado
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 7f7473f18f..9e0feb9d18 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -2,8 +2,8 @@
 es:
   about:
     about_mastodon_html: 'La red social del futuro: ¡Sin anuncios, sin vigilancia corporativa, diseño ético, y descentralización! ¡Sé dueño de tu información con Mastodon!'
-    contact_missing: No especificado
-    contact_unavailable: No disponible
+    contact_missing: No establecido
+    contact_unavailable: N/D
     hosted_on: Mastodon alojado en %{domain}
     title: Acerca de
   accounts:
@@ -11,7 +11,7 @@ es:
       one: Seguidor
       other: Seguidores
     following: Siguiendo
-    instance_actor_flash: Esta cuenta es un actor virtual utilizado para representar al servidor en sí mismo y no a ningún usuario individual. Se utiliza para propósitos de la federación y no se debe suspender.
+    instance_actor_flash: Esta cuenta es un actor virtual utilizado para representar al propio servidor y no a ningún usuario individual. Se utiliza con fines de federación y no debe suspenderse.
     last_active: última conexión
     link_verified_on: La propiedad de este vínculo fue verificada el %{date}
     nothing_here: "¡No hay nada aquí!"
@@ -40,7 +40,7 @@ es:
       avatar: Avatar
       by_domain: Dominio
       change_email:
-        changed_msg: "¡Email cambiado con éxito!"
+        changed_msg: "¡Correo electrónico cambiado con éxito!"
         current_email: Correo electrónico actual
         label: Cambiar el correo electrónico
         new_email: Nuevo correo electrónico
@@ -64,7 +64,7 @@ es:
       disable_sign_in_token_auth: Deshabilitar la autenticación por token de correo electrónico
       disable_two_factor_authentication: Desactivar autenticación de dos factores
       disabled: Deshabilitada
-      display_name: Nombre
+      display_name: Nombre para mostrar
       domain: Dominio
       edit: Editar
       email: Correo electrónico
@@ -102,7 +102,7 @@ es:
       moderation_notes: Notas de moderación
       most_recent_activity: Actividad más reciente
       most_recent_ip: IP más reciente
-      no_account_selected: Ninguna cuenta se cambió, ya que ninguna fue seleccionada
+      no_account_selected: No se ha modificado ninguna cuenta porque no se ha seleccionado ninguna
       no_limits_imposed: Sin límites impuestos
       no_role_assigned: Ningún rol asignado
       not_subscribed: No se está suscrito
@@ -309,6 +309,7 @@ es:
       title: Registro de auditoría
       unavailable_instance: "(nombre de dominio no disponible)"
     announcements:
+      back: Volver a la sección de anuncios
       destroyed_msg: "¡Anuncio eliminado con éxito!"
       edit:
         title: Editar anuncio
@@ -317,6 +318,10 @@ es:
       new:
         create: Crear anuncio
         title: Nuevo anuncio
+      preview:
+        disclaimer: Como los usuarios no pueden optar por no recibirlas, las notificaciones por correo electrónico deben limitarse a anuncios importantes, como la violación de datos personales o las notificaciones de cierre de servidores.
+        explanation_html: 'El correo electrónico se enviará a <strong>%{display_count} usuarios</strong>. En el correo electrónico se incluirá el siguiente texto:'
+        title: Vista previa de la notificación del anuncio
       publish: Publicar
       published_msg: "¡Anuncio publicado con éxito!"
       scheduled_for: Programado para %{time}
@@ -475,6 +480,36 @@ es:
       new:
         title: Importar bloqueos de dominio
       no_file: Ningún archivo seleccionado
+    fasp:
+      debug:
+        callbacks:
+          created_at: Creado el
+          delete: Eliminar
+          ip: Dirección IP
+          request_body: Cuerpo de la solicitud
+          title: Depurar devoluciones de llamada
+      providers:
+        active: Activo
+        base_url: URL base
+        callback: Devolución de llamada
+        delete: Eliminar
+        edit: Editar proveedor
+        finish_registration: Terminar el registro
+        name: Nombre
+        providers: Proveedores
+        public_key_fingerprint: Huella digital de clave pública
+        registration_requested: Se solicitó el registro
+        registrations:
+          confirm: Confirmar
+          description: Has recibido un registro de un FASP. Recházalo si no lo iniciaste tú. Si lo iniciaste tú, compara cuidadosamente el nombre y la huella de la clave antes de confirmar el registro.
+          reject: Rechazar
+          title: Confirmar registro FASP
+        save: Guardar
+        select_capabilities: Selecciona las capacidades
+        sign_in: Iniciar sesión
+        status: Estado
+        title: Fediverse Auxiliary Service Providers (Proveedores de Servicio Auxiliares del Fediverso)
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Las recomendaciones de cuentas ayudan a los nuevos usuarios a encontrar rápidamente contenido interesante</strong>. Cuando un usuario no ha interactuado con otros lo suficiente como para suscitar recomendaciones personalizadas de cuentas a las que seguir, en su lugar se le recomiendan estas cuentas. Se recalculan diariamente a partir de una mezcla de cuentas con el mayor número de interacciones recientes y con el mayor número de seguidores locales con un idioma determinado."
       language: Para el idioma
@@ -939,6 +974,7 @@ es:
         chance_to_review_html: "<strong>Los términos del servicio generados no se publicarán automáticamente..</strong> Tendrás la oportunidad de revisar el resultado. Por favor, rellena los detalles necesarios para continuar."
         explanation_html: La plantilla de términos de servicio ofrecida es únicamente para propósito informativo, y no debería ser considerada asesoramiento legal sobre ningún tema. Por favor, consulta con tu propio consejo legal sobre tu situación y las cuestiones legales específicas que tengas.
         title: Configuración de términos del servicio
+      going_live_on_html: En vigor desde %{date}
       history: Historial
       live: En vivo
       no_history: Aún no se han registrado cambios en los términos del servicio.
@@ -1904,6 +1940,10 @@ es:
     recovery_instructions_html: Si pierdes acceso a tu teléfono, puedes usar uno de los siguientes códigos de recuperación para obtener acceso a tu cuenta. <strong>Mantenlos a salvo</strong>. Por ejemplo, puedes imprimirlos y guardarlos con otros documentos importantes.
     webauthn: Claves de seguridad
   user_mailer:
+    announcement_published:
+      description: 'Los administradores de %{domain} están haciendo un anuncio:'
+      subject: Anuncio
+      title: Anuncio de %{domain}
     appeal_approved:
       action: Ajustes de la cuenta
       explanation: La apelación de la amonestación contra tu cuenta del %{strike_date} que enviaste el %{appeal_date} fue aprobada. Tu cuenta se encuentra de nuevo en buen estado.
@@ -1936,8 +1976,8 @@ es:
     terms_of_service_changed:
       agreement: Al seguir usando %{domain}, aceptas estos términos. Si no estás de acuerdo con los términos actualizados, puedes cancelar tu acuerdo con %{domain} en cualquier momento eliminando tu cuenta.
       changelog: 'En resumen, esto es lo que esta actualización significa para ti:'
-      description: 'Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos del servicio en %{domain}. Te animamos a revisar los términos actualizados en su totalidad aquí:'
-      description_html: Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos del servicio en %{domain}. Te animamos a revisar los <a href="%{path}" target="_blank">términos actualizados en su totalidad aquí</a>.
+      description: 'Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos de servicio en %{domain}. Estas actualizaciones entrarán en vigor en %{date}. Te animamos a revisar los términos actualizados en su totalidad aquí:'
+      description_html: Estás recibiendo este correo electrónico porque estamos haciendo algunos cambios en nuestros términos de servicio en %{domain}. Estas actualizaciones entrarán en vigor en <strong>%{date}</strong>. Te animamos a revisar los <a href="%{path}" target="_blank"> términos actualizados en su totalidad aquí</a>.
       sign_off: El equipo de %{domain}
       subject: Actualizaciones en nuestros términos del servicio
       subtitle: Los términos del servicio de %{domain} están cambiando
diff --git a/config/locales/et.yml b/config/locales/et.yml
index 5d83333da8..8339ed087f 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -1159,6 +1159,7 @@ et:
     set_new_password: Uue salasõna määramine
     setup:
       email_below_hint_html: Kontrolli rämpsposti kausta või taotle uut. Saad oma e-posti aadressi parandada, kui see on vale.
+      email_settings_hint_html: Klõpsa aadressile %{email} saadetud linki, et alustada Mastodoni kasutamist. Me oleme ootel.
       link_not_received: Kas ei saanud linki?
       new_confirmation_instructions_sent: Saad mõne minuti pärast uue kinnituslingiga e-kirja!
       title: Kontrolli sisendkasti
@@ -1167,6 +1168,7 @@ et:
       title: Logi sisse kohta %{domain}
     sign_up:
       manual_review: Liitumised kohas %{domain} vaadatakse meie moderaatorite poolt käsitsi läbi. Aitamaks meil sinu taotlust läbi vaadata, kirjuta palun natuke endast ja miks soovid kontot kohas %{domain}.
+      preamble: Selle Mastodoni serveri kontoga saad jälgida mistahes teist isikut fediversumis, sõltumata sellest, kus ta konto on majutatud.
       title: Loo konto serverisse  %{domain}.
     status:
       account_status: Konto olek
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 8a5d36e1e3..f1c74829c9 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -291,7 +291,7 @@ fa:
         silence_account_html: "%{name} حساب %{target} را محدود کرد"
         suspend_account_html: "%{name} حساب %{target} را تعلیق کرد"
         unassigned_report_html: "%{name} گزارش %{target} را از حالت محول شده خارج کرد"
-        unblock_email_account_html: "%{name} نشانی رایانامهٔ %{target} را رفع مسدودیت کرد"
+        unblock_email_account_html: "%{name} مسدودی نشانی رایانامهٔ %{target} را رفع کرد"
         unsensitive_account_html: "%{name} علامت حساس رسانهٔ %{target} را برداشت"
         unsilence_account_html: "%{name} محدودیت حساب %{target} را برداشت"
         unsuspend_account_html: "%{name} حساب %{target} را از تعلیق خارج کرد"
@@ -309,6 +309,7 @@ fa:
       title: سیاههٔ بازرسی
       unavailable_instance: "(نام دامنه ناموجود)"
     announcements:
+      back: بازگشت به اعلامیه‌ها
       destroyed_msg: اعلامیه با موفقیت حذف شد!
       edit:
         title: ویرایش اعلامیه
@@ -317,6 +318,9 @@ fa:
       new:
         create: ساختن اعلامیه
         title: اعلامیهٔ تازه
+      preview:
+        explanation_html: 'رایانامه برای <strong>%{display_count} کاربر</strong> فرستاده خواهد شد. متن زیر در رایانامه قرار خواهد گرفت:'
+        title: پیش‌نمایش آگاهی اعلامیه
       publish: انتشار
       published_msg: اعلامیه با موفقیت منتشر شد!
       scheduled_for: زمان‌بسته برای %{time}
@@ -457,7 +461,7 @@ fa:
         title: مسدودسازی دامنهٔ رایانامهٔ جدید
       no_email_domain_block_selected: هیچ انسداد دامنهٔ رایانامه‌ای تغییر نکرد زیرا هیچ‌کدامشان انتخاب نشده بودند
       not_permitted: مجاز نیست
-      resolved_dns_records_hint_html: نام دامنه به دامنه های MX زیر منتقل می شود که در نهایت مسئولیت پذیرش ایمیل را بر عهده دارند. مسدود کردن دامنه MX، ثبت نام از هر آدرس ایمیلی را که از همان دامنه MX استفاده می کند، مسدود می کند، حتی اگر نام دامنه قابل مشاهده متفاوت باشد. <strong>مراقب باشید ارائه دهندگان ایمیل اصلی را مسدود نکنید.</strong>
+      resolved_dns_records_hint_html: نام دامنه به دامنه‌های MX زیر منتقل می شود که در نهایت مسئولیت پذیرش رایانامه را بر عهده دارند. انسداد دامنهٔ MX، ثبت‌نام از هر نشانی رایانامه‌ای را که از همان دامنهٔ MX استفاده می‌کند را مسدود می‌کند؛ حتا اگر نام دامنهٔ نمایان متفاوت باشد. <strong>مراقب باشید ارائه‌دهندگان رایانامهٔ بزرگ را مسدود نکنید.</strong>
       resolved_through_html: از طریق %{domain} حل شد
       title: دامنه‌های رایانامهٔ مسدود شده
     export_domain_allows:
@@ -475,6 +479,22 @@ fa:
       new:
         title: درون‌ریزی انسدادهای دامنه
       no_file: هیچ پرونده‌ای گزیده نشده
+    fasp:
+      debug:
+        callbacks:
+          delete: حذف
+      providers:
+        active: فعال
+        delete: حذف
+        finish_registration: تکمیل ثبت‌نام
+        name: نام
+        providers: ارائه دهندگان
+        registrations:
+          confirm: تایید
+          reject: رد کردن
+        save: ذخیره
+        sign_in: ورود
+        status: وضعیت
     follow_recommendations:
       description_html: "<strong>پیشنهادات پیگیری به کاربران جدید کک می‌کند تا سریع‌تر محتوای جالب را پیدا کنند</strong>. زمانی که کاربری هنوز به اندازه کافی با دیگران تعامل نداشته است تا پیشنهادات پیگیری شخصی‌سازی‌شده دریافت کند، این حساب‌ها را به جای آن فهرست مشاهده خواهد کرد. این حساب‌ها به صورت روزانه و در ترکیب با بیشتری تعاملات و بالاترین دنبال‌کنندگان محلی برای یک زبان مشخص بازمحاسبه می‌شوند."
       language: برای زبان
@@ -613,7 +633,7 @@ fa:
         resolve_description_html: هیچ کنشی علیه حساب گزارش شده انجام نخواهد شد. هیچ شکایتی ضبط نشده و گزارش بسته خواهد شد.
         silence_description_html: این حساب فقط برای کسانی قابل مشاهده خواهد بود که قبلاً آن را دنبال می کنند یا به صورت دستی آن را جستجو می کنند و دسترسی آن را به شدت محدود می کند. همیشه می توان برگرداند. همه گزارش‌های مربوط به این حساب را می‌بندد.
         suspend_description_html: اکانت و تمامی محتویات آن غیرقابل دسترسی و در نهایت حذف خواهد شد و تعامل با آن غیر ممکن خواهد بود. قابل برگشت در عرض 30 روز همه گزارش‌های مربوط به این حساب را می‌بندد.
-      actions_description_html: تصمیم بگیرید که چه اقدامی برای حل این گزارش انجام دهید. اگر اقدام تنبیهی علیه حساب گزارش شده انجام دهید، یک اعلان ایمیل برای آنها ارسال می شود، به جز زمانی که دسته <strong>هرزنامه</strong> انتخاب شده باشد.
+      actions_description_html: تصمیم گیری کنش اقدامی برای حل این گزارش. در صورت انجام کنش تنبیهی روی حساب گزارش شده، غیر از زمان یکه دستهٔ <strong>هرزنامه</strong> گزیده باشد، برایش آگاهی رایانامه‌ای فرستاده خواهد شد.
       actions_description_remote_html: تصمیم بگیرید که چه اقدامی برای حل این گزارش انجام دهید. این فقط بر نحوه ارتباط سرور <strong>شما</strong> با این حساب راه دور و مدیریت محتوای آن تأثیر می گذارد.
       actions_no_posts: این گزارش هیچ پست مرتبطی برای حذف ندارد
       add_to_report: افزودن بیش‌تر به گزارش
@@ -679,7 +699,7 @@ fa:
         delete_data_html: نمایه و محتویات <strong>@%{acct}</strong> را 30 روز بعد حذف کنید، مگر اینکه در این مدت معلق نشوند
         preview_preamble_html: "<strong>@%{acct}</strong> اخطاری با محتوای زیر دریافت خواهد کرد:"
         record_strike_html: ضبط شکایتی علیه <strong>‪@%{acct}‬</strong> برای کمک به تصمیم‌گیری برای قانون‌شکنی‌های آیندهٔ این حساب
-        send_email_html: یک ایمیل هشدار به <strong>@%{acct}</strong> ارسال کنید
+        send_email_html: فرستادن رایانامهٔ هشدار به ‪<strong>@%{acct}</strong>‬
         warning_placeholder: استدلال اضافی اختیاری برای اقدام تعدیل.
       target_origin: خاستگاه حساب گزارش‌شده
       title: گزارش‌ها
@@ -719,7 +739,7 @@ fa:
         manage_appeals: مدیریت درخواست‌های بازنگری
         manage_appeals_description: به کاربران امکان می‌دهد درخواست‌های تجدیدنظر علیه اقدامات تعدیل را بررسی کنند
         manage_blocks: مدیریت مسدودی‌ها
-        manage_blocks_description: به کاربران اجازه می دهد تا ارائه دهندگان ایمیل و آدرس های آی پی را مسدود کنند
+        manage_blocks_description: می‌گذارد کاربران فراهم‌کنندگان رایانامه و نشانی‌های آی‌پی را مسدود کنند
         manage_custom_emojis: مدیریت ایموجی‌های سفارشی
         manage_custom_emojis_description: به کاربران اجازه می دهد تا ایموجی های سفارشی را روی سرور مدیریت کنند
         manage_federation: مدیریت خودگردانی
@@ -735,9 +755,9 @@ fa:
         manage_settings: مدیریت تنظیمات
         manage_settings_description: اجازه به کاربران برای تغییر تنظیمات پایگاه
         manage_taxonomies: مدیریت طیقه‌بندی‌ها
-        manage_taxonomies_description: به کاربران امکان می‌دهد محتوای پرطرفدار را بررسی کنند و تنظیمات هشتگ را به‌روزرسانی کنند
+        manage_taxonomies_description: می‌گذارد کاربران محتوای داغ را بررسی و تنظیمات برچسب را به‌روز کنند
         manage_user_access: مدیریت دسترسی کاربران
-        manage_user_access_description: به کاربران اجازه می دهد تا احراز هویت دو مرحله ای سایر کاربران را غیرفعال کنند، آدرس ایمیل آنها را تغییر دهند و رمز عبور خود را بازنشانی کنند
+        manage_user_access_description: می‌گذارد کاربران هویت‌سنجی دو مرحله‌ای دیگر کاربران را از کار انداخته، نشانی رایانامه‌شان را تغییر داده و گذرواژه‌شان را بازنشانی کنند
         manage_users: مدیریت کاربران
         manage_users_description: به کاربران اجازه می دهد تا جزئیات سایر کاربران را مشاهده کنند و اقدامات تعدیل را علیه آنها انجام دهند
         manage_webhooks: مدیریت قلّاب‌های وب
@@ -812,7 +832,7 @@ fa:
       destroyed_msg: بارگذاری پایگاه با موفقیت حذف شد!
     software_updates:
       critical_update: بحرانی — لطفاً به سرعت به‌روز کنید
-      description: توصیه می شود نصب ماستودون خود را به روز نگه دارید تا از آخرین اصلاحات و ویژگی ها بهره مند شوید. علاوه بر این، گاهی اوقات برای جلوگیری از مشکلات امنیتی، به روز رسانی ماستودون به موقع ضروری است. به این دلایل، ماستودون هر 30 دقیقه یکبار به‌روزرسانی‌ها را بررسی می‌کند و طبق اولویت‌های اعلان ایمیل شما را مطلع می‌کند.
+      description: توصیه می‌شود نصب ماستودونتان را به‌روز نگه داشته تا از جدیدترین اصلاحات و ویژگی‌ها بهره‌مند شوید. همچنین گاهی به‌روز رسانی به‌موقع ماستودون برای پیشگیری از مشکلات امنیتی ضروریست. برای همین ماستودون هر ۳۰ دقیقه به‌روز رسانی‌ها را بررسی کرده و بنا به ترجیحات آگاهی رایانامه‌ایتان آگاهتان خواهد کرد.
       documentation_link: بیش‌تر بیاموزید
       release_notes: یادداشت‌های انتشار
       title: به‌روز رسانی‌های موجود
@@ -939,6 +959,7 @@ fa:
         chance_to_review_html: "<strong>شرایط خدمات ایجاد شده به صورت خودکار منتشر نخواهد شد.</strong> شما فرصتی خواهید داشت که نتایج را بررسی کنید. لطفا مشخصات لازم را برای ادامه وارد کنید."
         explanation_html: الگوی شرایط خدمات ارائه شده فقط برای اهداف اطلاعاتی است و نباید به عنوان مشاوره حقوقی در مورد هر موضوعی تلقی شود. لطفاً در مورد وضعیت خود و سؤالات حقوقی خاصی که دارید با مشاور حقوقی خود مشورت کنید.
         title: راه اندازی شرایط خدمات
+      going_live_on_html: زنده. اعمال شده از %{date}
       history: تاریخچه
       live: زنده
       no_history: هنوز هیچ تغییری در شرایط خدمات ثبت نشده است.
@@ -946,11 +967,11 @@ fa:
       notified_on_html: کاربران در %{date} مطلع شدند
       notify_users: به کاربران اطلاع دهید
       preview:
-        explanation_html: 'ایمیل برای <strong>%{display_count} کاربران</strong> که قبل از %{date} ثبت نام کرده اند ارسال خواهد شد. متن زیر در ایمیل درج خواهد شد:'
+        explanation_html: 'رایانامه برای <strong>%{display_count} کاربری</strong> که پیش از %{date} ثبت‌نام کرده‌اند فرستاده خواهد شد. متن زیر در رایانامه قرار خواهد گرفت:'
         send_preview: ارسال پیش نمایش به %{email}
         send_to_all:
           one: "%{display_count} ایمیل ارسال کنید"
-          other: "%{display_count} ایمیل ارسال کنید"
+          other: فرستادن %{display_count} رایانامه
         title: پیش نمایش اعلان شرایط خدمات
       publish: انتشار دهید
       published_on_html: منتشر شده در %{date}
@@ -1136,7 +1157,7 @@ fa:
       hint_html: فقط یک چیز دیگر! ما باید تأیید کنیم که شما یک انسان هستید (این برای جلوگیری از هرزنامه است!). کپچا زیر را حل کنید و روی "ادامه" کلیک کنید.
       title: بررسی های امنیتی
     confirmations:
-      awaiting_review: آدرس ایمیل شما تایید شد! کارکنان %{domain} اکنون در حال بررسی ثبت نام شما هستند. اگر حساب شما را تایید کنند یک ایمیل دریافت خواهید کرد!
+      awaiting_review: نشانی رایانامه‌تان تأیید شد! عوامل %{domain} دارند ثبت‌نامتان را بررسی می‌کنند. در صورت تأیید حسابتان رایانامه‌ای خواهید گرفت!
       awaiting_review_title: ثبت‌نامتان دارد بررسی می‌شود
       clicking_this_link: زدن این پیوند
       login_link: ورود
@@ -1144,7 +1165,7 @@ fa:
       redirect_to_app_html: باید به برنامه <strong>%{app_name}</strong> هدایت می‌شوید. اگر این اتفاق نیفتاد، %{clicking_this_link} را امتحان کنید یا به صورت دستی به برنامه برگردید.
       registration_complete: ثبت نام شما در %{domain} اکنون کامل شده است!
       welcome_title: خوش آمدید، %{name}!
-      wrong_email_hint: اگر آن آدرس ایمیل درست نیست، می‌توانید آن را در تنظیمات حساب تغییر دهید.
+      wrong_email_hint: اگر نشانی رایانامه درست نیست می‌توانید در تنظیمات حساب تغییرش دهید.
     delete_account: پاک‌کردن حساب
     delete_account_html: اگر می‌خواهید حساب خود را پاک کنید، از <a href="%{path}">این‌جا</a> پیش بروید. از شما درخواست تأیید خواهد شد.
     description:
@@ -1186,10 +1207,10 @@ fa:
     security: امنیت
     set_new_password: تعین گذرواژه جدید
     setup:
-      email_below_hint_html: پوشه هرزنامه خود را بررسی کنید یا یک پوشه دیگر درخواست کنید. در صورت اشتباه می توانید آدرس ایمیل خود را تصحیح کنید.
+      email_below_hint_html: بررسی شاخهٔ هرزنامه یا درخواست رایانامه‌ای دیگر. در صورت اشتباه بودن نشانی رایانامه می‌توانید تصحیحش کنید.
       email_settings_hint_html: برای شروع استفاده از ماستودون روی پیوندی که به %{email} فرستادیم کلیک کنید. همین جا منتظر می مانیم.
       link_not_received: پیوندی نگرفتید؟
-      new_confirmation_instructions_sent: تا چند دقیقه دیگر یک ایمیل جدید با لینک تایید دریافت خواهید کرد!
+      new_confirmation_instructions_sent: تا چند دقیقهٔ دیگر رایانامه‌ای جدید با پیوند تأییدیه خواهید گرفت!
       title: صندوق ورودیتان را بررسی کنید
     sign_in:
       preamble_html: با اطلاعات کاربری <strong>%{domain}</strong> خود وارد شوید. اگر حساب شما روی سرور دیگری میزبانی شود، نمی توانید در اینجا وارد شوید.
@@ -1522,7 +1543,7 @@ fa:
     unsubscribe:
       action: بله. لغو اشتراک
       complete: لغو اشتراک شد
-      confirmation_html: آیا مطمئنید که می‌خواهید از دریافت %{type} برای ماستودون در %{domain} به ایمیل خود در %{email} لغو اشتراک کنید؟ همیشه می‌توانید از <a href="%{settings_path}">تنظیمات اعلان ایمیل</a> خود مجدداً مشترک شوید.
+      confirmation_html: مطمئنید که می‌خواهید اشتراک %{type} را از ماستودون روی %{domain} برای رایانامهٔ %{email} لغو کنید؟ همواره می‌توانید از <a href="%{settings_path}">تنظیمات آگاهی رایانامه‌ای</a> دوباره مشترک شوید.
       emails:
         notification_emails:
           favourite: رایانامه‌های آگاهی برگزیدن
@@ -1530,8 +1551,8 @@ fa:
           follow_request: رایانامه‌های درخواست پی‌گیری
           mention: رایانامه‌های آگاهی اشاره
           reblog: رایانامه‌های آگاهی تقویت
-      resubscribe_html: اگر به اشتباه اشتراک را لغو کرده‌اید، می‌توانید از <a href="%{settings_path}">تنظیمات اعلان ایمیل</a> خود مجدداً اشتراک کنید.
-      success_html: دیگر %{type} را برای ماستودون در %{domain} به ایمیل خود در %{email} دریافت نخواهید کرد.
+      resubscribe_html: اگر اشتراک را اشتباهی لغو کردید می‌توانید از <a href="%{settings_path}">تنظیمات آگاهی رایانامه‌ای</a> دوباره مشترک شوید.
+      success_html: دیگر %{type} را از ماستودون روی %{domain} برای رایانامهٔ %{email} نخواهید گرفت.
       title: لغو اشتراک
   media_attachments:
     validations:
@@ -1904,6 +1925,10 @@ fa:
     recovery_instructions_html: اگر تلفن خود را گم کردید، می‌توانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. <strong>این کدها را در جای امنی نگه دارید.</strong> مثلاً آن‌ها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید.
     webauthn: کلیدهای امنیتی
   user_mailer:
+    announcement_published:
+      description: 'مدیران %{domain} اعلامیه‌ای داده‌اند:'
+      subject: اعلامیهٔ خدمت
+      title: اعلامیهٔ خدمت %{domain}
     appeal_approved:
       action: تنظیمات حساب
       explanation: درخواست تجدیدنظر اخطار علیه حساب شما در %{strike_date} که در %{appeal_date} ارسال کرده‌اید، پذیرفته شده است. حساب شما بار دیگر در وضعیت خوبی قرار دارد.
@@ -1936,8 +1961,8 @@ fa:
     terms_of_service_changed:
       agreement: با ادامه استفاده از %{domain}، با این شرایط موافقت می کنید. اگر با شرایط به‌روزرسانی شده مخالف هستید، می‌توانید در هر زمان با حذف حساب خود، قرارداد خود را با %{domain} فسخ کنید.
       changelog: 'در یک نگاه، معنای این به‌روزرسانی برای شما چیست:'
-      description: 'شما این ایمیل را دریافت می کنید زیرا ما در حال ایجاد برخی تغییرات در شرایط خدمات خود در %{domain} هستیم. توصیه می کنیم شرایط به روز شده را به طور کامل در اینجا مرور کنید:'
-      description_html: شما این ایمیل را دریافت می کنید زیرا ما در حال ایجاد برخی تغییرات در شرایط خدمات خود در %{domain} هستیم. توصیه می کنیم <a href="%{path}" target="_blank">شرایط به روز شده را به طور کامل در اینجا بررسی کنید</a>.
+      description: 'این رایانانه را می‌گیرید چرا که دارین اغییراتی در شرایط خدمتتان در %{domain} می‌دهیم. این به‌روز رسانی‌ها از %{date} اعمال خواهند شد. توصیه می‌کنیم شرایط به‌روز شده را به طور کامل در این‌جا بازبینی کنید:'
+      description_html: این رایانانه را می‌گیرید چرا که دارین اغییراتی در شرایط خدمتتان در %{domain} می‌دهیم. این به‌روز رسانی‌ها از <strong>%{date}</strong> اعمال خواهند شد. توصیه می‌کنیم شرایط به‌روز شده را به طور کامل در <a href="%{path}" target="_blank">این‌جا</a> بازبینی کنید.
       sign_off: تیم %{domain}
       subject: به‌روزرسانی‌های شرایط خدمات ما
       subtitle: شرایط خدمات %{domain} در حال تغییر است
@@ -2018,7 +2043,7 @@ fa:
     follow_limit_reached: شما نمی‌توانید بیش از %{limit} نفر را پی بگیرید
     go_to_sso_account_settings: به تنظیمات حساب فراهمگر هوبتتان بروید
     invalid_otp_token: کد ورود دومرحله‌ای نامعتبر است
-    otp_lost_help_html: اگر شما دسترسی به هیچ‌کدامشان ندارید، باید با ایمیل %{email} تماس بگیرید
+    otp_lost_help_html: اگر به هیچ‌کدامشان دسترسی ندارید با %{email} تماس بگیرید
     rate_limited: تلاش ّای هویت‌سنجی بیش از حد. لطفاً بعداً دوباره تلاش کنید.
     seamless_external_login: با خدمتی خارجی وارد شده‌اید، برای همین تنظیمات رایانامه و گذرواژه در دسترس نیستند.
     signed_in_as: 'واردشده به نام:'
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index c7148772b8..9a5bf97afe 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -10,7 +10,7 @@ fi:
     followers:
       one: seuraaja
       other: seuraajaa
-    following: seurattu(a)
+    following: Seurattavat
     instance_actor_flash: Tämä tili on virtuaalinen toimija, jota käytetään edustamaan itse palvelinta eikä yksittäistä käyttäjää. Sitä käytetään federointitarkoituksiin, eikä sitä tule jäädyttää.
     last_active: viimeksi aktiivinen
     link_verified_on: Tämän linkin omistus on tarkastettu %{date}
@@ -74,7 +74,7 @@ fi:
       enabled: Käytössä
       enabled_msg: Käyttäjän %{username} tilin jäädytys kumottiin onnistuneesti
       followers: Seuraajat
-      follows: Seuratut
+      follows: Seurattavat
       header: Otsakekuva
       inbox_url: Postilaatikon osoite
       invite_request_text: Syitä liittymiseen
@@ -309,6 +309,7 @@ fi:
       title: Tarkastusloki
       unavailable_instance: "(verkkotunnus ei saatavilla)"
     announcements:
+      back: Takaisin tiedotteisiin
       destroyed_msg: Tiedotteen poisto onnistui!
       edit:
         title: Muokkaa tiedotetta
@@ -317,6 +318,10 @@ fi:
       new:
         create: Luo tiedote
         title: Uusi tiedote
+      preview:
+        disclaimer: Koska käyttäjät eivät voi kieltäytyä niistä, sähköposti-ilmoitukset tulee rajata tärkeisiin tiedotteisiin, kuten ilmoituksiin henkilötietojen tietoturvaloukkauksista tai palvelimen sulkeutumisesta.
+        explanation_html: "<strong>%{display_count} käyttäjälle</strong> lähetetään sähköpostia. Sähköpostiviestiin sisällytetään seuraava teksti:"
+        title: Esikatsele tiedoteilmoitus
       publish: Julkaise
       published_msg: Tiedotteen julkaisu onnistui!
       scheduled_for: Ajoitettu %{time}
@@ -453,7 +458,7 @@ fi:
       domain: Verkkotunnus
       new:
         create: Lisää verkkotunnus
-        resolve: Selvitä verkkotunnus
+        resolve: Resolvoi verkkotunnus
         title: Estä uusi sähköpostiverkkotunnus
       no_email_domain_block_selected: Sähköpostiverkkotunnusten estoja ei muutettu, koska yhtäkään ei ollut valittuna
       not_permitted: Ei sallittu
@@ -475,6 +480,24 @@ fi:
       new:
         title: Tuo verkkotunnusten estoja
       no_file: Yhtäkään tiedostoa ei ole valittu
+    fasp:
+      debug:
+        callbacks:
+          created_at: Luotu
+          delete: Poista
+          ip: IP-osoite
+      providers:
+        delete: Poista
+        finish_registration: Viimeistele rekisteröinti
+        name: Nimi
+        public_key_fingerprint: Julkisen avaimen sormenjälki
+        registration_requested: Rekisteröintiä pyydetty
+        registrations:
+          confirm: Vahvista
+          reject: Hylkää
+        save: Tallenna
+        sign_in: Kirjaudu sisään
+        status: Tila
     follow_recommendations:
       description_html: "<strong>Seurantasuositukset auttavat uusia käyttäjiä löytämään nopeasti kiinnostavaa sisältöä</strong>. Kun käyttäjä ei ole ollut tarpeeksi vuorovaikutuksessa muiden kanssa, jotta hänelle olisi muodostunut henkilökohtaisia seuraamissuosituksia, suositellaan niiden sijaan näitä tilejä. Ne lasketaan päivittäin uudelleen yhdistelmästä tilejä, jotka ovat viime aikoina olleet aktiivisimmin sitoutuneita ja joilla on suurimmat paikalliset seuraajamäärät tietyllä kielellä."
       language: Kielelle
@@ -939,6 +962,7 @@ fi:
         chance_to_review_html: "<strong>Luotuja käyttöehtoja ei julkaista automaattisesti.</strong> Sinulla on mahdollisuus tarkistaa lopputulos. Jatka täyttämällä tarvittavat tiedot."
         explanation_html: Tarjottu käyttöehtomalli on tarkoitettu vain tiedoksi, eikä sitä pidä tulkita oikeudellisena neuvontana missään yhteydessä. Käänny oman oikeusavustajasi puoleen tilanteessasi ja erityisissä oikeudellisissa kysymyksissäsi.
         title: Käyttöehtojen määritys
+      going_live_on_html: Voimassa %{date} alkaen
       history: Historia
       live: Julki
       no_history: Käyttöehtoihin ei ole vielä tehty muutoksia.
@@ -1466,18 +1490,18 @@ fi:
       blocking: Tuodaan estettyjä tilejä
       bookmarks: Tuodaan kirjanmerkkejä
       domain_blocking: Tuodaan estettyjä verkkotunnuksia
-      following: Tuodaan seurattuja tilejä
+      following: Tuodaan seurattavia tilejä
       lists: Tuodaan listoja
       muting: Tuodaan mykistettyjä tilejä
     type: Tuontityyppi
     type_groups:
-      constructive: Seuratut ja kirjanmerkit
+      constructive: Seurattavat ja kirjanmerkit
       destructive: Estot ja mykistykset
     types:
       blocking: Estoluettelo
       bookmarks: Kirjanmerkit
       domain_blocking: Verkkotunnusten estoluettelo
-      following: Seurattujen luettelo
+      following: Seurattavien luettelo
       lists: Listat
       muting: Mykistysluettelo
     upload: Lähetä
@@ -1683,7 +1707,7 @@ fi:
     follow_failure: Joitain valittuja tilejä ei voitu seurata.
     follow_selected_followers: Seuraa valittuja seuraajia
     followers: Seuraajat
-    following: Seuratut
+    following: Seurattavat
     invited: Kutsutut
     last_active: Viimeksi aktiivinen
     most_recent: Tuorein
@@ -1776,7 +1800,7 @@ fi:
     notifications: Sähköposti-ilmoitukset
     preferences: Asetukset
     profile: Julkinen profiili
-    relationships: Seuratut ja seuraajat
+    relationships: Seurattavat ja seuraajat
     severed_relationships: Katkenneet suhteet
     statuses_cleanup: Julkaisujen automaattipoisto
     strikes: Moderointivaroitukset
@@ -1789,8 +1813,8 @@ fi:
       domain_block: Palvelimen jäädytys (%{target_name})
       user_domain_block: Estit käyttäjän %{target_name}
     lost_followers: Menetetyt seuraajat
-    lost_follows: Menetetyt seuratut
-    preamble: Voit menettää seurattusi ja seuraajasi, kun estät verkkotunnuksen tai kun moderaattorisi päättävät jäädyttää etäpalvelimen. Kun näin tapahtuu, voit ladata luetteloita katkenneista seurantasuhteista, jotta voit tarkastella niitä ja mahdollisesti viedä ne toiselle palvelimelle.
+    lost_follows: Menetetyt seurattavat
+    preamble: Voit menettää seurattavasi ja seuraajasi, kun estät verkkotunnuksen tai kun moderaattorisi päättävät jäädyttää etäpalvelimen. Kun näin tapahtuu, voit ladata luetteloita katkenneista seurantasuhteista, jotta voit tarkastella niitä ja mahdollisesti viedä ne toiselle palvelimelle.
     purged: Palvelimesi ylläpitäjät ovat tyhjentäneet tämän palvelimen tiedot.
     type: Tapahtuma
   statuses:
@@ -1904,6 +1928,10 @@ fi:
     recovery_instructions_html: Jos menetät puhelimesi, voit kirjautua tilillesi jollakin alla olevista palautuskoodeista. <strong>Pidä palautuskoodit hyvässä tallessa</strong>. Voit esimerkiksi tulostaa ne ja säilyttää muiden tärkeiden papereiden joukossa.
     webauthn: Suojausavaimet
   user_mailer:
+    announcement_published:
+      description: 'Palvelimen %{domain} ylläpito tiedottaa:'
+      subject: Palvelutiedote
+      title: Palvelimen %{domain} palvelutiedote
     appeal_approved:
       action: Tilin asetukset
       explanation: "%{appeal_date} lähettämäsi valitus tiliisi kohdistuvasta varoituksesta %{strike_date} on hyväksytty. Tilisi on jälleen hyvässä kunnossa."
@@ -1936,8 +1964,8 @@ fi:
     terms_of_service_changed:
       agreement: Jatkamalla palvelun %{domain} käyttöä hyväksyt nämä ehdot. Jos et hyväksy päivitettyjä ehtoja, voit milloin tahansa päättää sopimuksesi palvelun %{domain} kanssa poistamalla tilisi.
       changelog: 'Lyhyesti, mitä tämä päivitys tarkoittaa sinulle:'
-      description: 'Sait tämän sähköpostiviestin, koska teemme muutoksia palvelun %{domain} käyttöehtoihin. Kehotamme sinua tutustumaan päivitettyihin ehtoihin kokonaisuudessaan täällä:'
-      description_html: Sait tämän sähköpostiviestin, koska teemme muutoksia palvelun %{domain} käyttöehtoihin. Kehotamme sinua tutustumaan <a href="%{path}" target="_blank">päivitettyihin ehtoihin kokonaisuudessaan täällä</a>.
+      description: 'Sait tämän sähköpostiviestin, koska muutamme palvelun %{domain} käyttöehtoja. Muutokset tulevat voimaan %{date}. Kehotamme tutustumaan päivitettyihin ehtoihin kokonaisuudessaan täällä:'
+      description_html: Sait tämän sähköpostiviestin, koska muutamme palvelun %{domain} käyttöehtoja. Muutokset tulevat voimaan <strong>%{date}</strong>. Kehotamme tutustumaan <a href="%{path}" target="_blank">päivitettyihin ehtoihin kokonaisuudessaan täällä</a>.
       sign_off: Palvelimen %{domain} tiimi
       subject: Käyttöehtojemme päivitykset
       subtitle: Palvelimen %{domain} käyttöehdot muuttuvat
diff --git a/config/locales/fo.yml b/config/locales/fo.yml
index 93bf30e0ef..8e611c346d 100644
--- a/config/locales/fo.yml
+++ b/config/locales/fo.yml
@@ -309,6 +309,7 @@ fo:
       title: Skoðanarloggur
       unavailable_instance: "(økisnavn ikki tøkt)"
     announcements:
+      back: Aftur til kunngerðir
       destroyed_msg: Kunngerð strikað!
       edit:
         title: Rætta kunngerð
@@ -317,6 +318,10 @@ fo:
       new:
         create: Stovna kunngerð
         title: Nýggj kunngerð
+      preview:
+        disclaimer: Av tí at brúkarar ikki kunnu velja tær frá, eiga teldupostfráboðanir at vera avmarkaðar til týdningarmiklar kunngerðir, sosum trygdarbrot og boð um at ambætarin verður tikin niður.
+        explanation_html: 'Teldubrævið verður sent til <strong>%{display_count} brúkarar</strong>. Fylgjandi tekstur kemur við í teldubrævið:'
+        title: Undanvís fráboðan um kunngerð
       publish: Legg út
       published_msg: Kunngerð útgivin!
       scheduled_for: Sett á skrá %{time}
@@ -475,6 +480,36 @@ fo:
       new:
         title: Innflyt navnaøkjablokeringar
       no_file: Eingin fíla vald
+    fasp:
+      debug:
+        callbacks:
+          created_at: Stovnað
+          delete: Strika
+          ip: IP adressa
+          request_body: Umbønskroppur
+          title: Kemb afturkøll
+      providers:
+        active: Virkin
+        base_url: Grund-URL
+        callback: Afturkall
+        delete: Strika
+        edit: Rætta veitara
+        finish_registration: Enda skráseting
+        name: Navn
+        providers: Veitarar
+        public_key_fingerprint: Fingramerki á almennum lykli
+        registration_requested: Skráseting umbiðin
+        registrations:
+          confirm: Vátta
+          description: Tú hevur móttikið eina skráseting frá einum FASP. Avvís, um tú ikki setti hetta í gongd. Um tú setti tað í gongd, so skal tú vandaliga samanbera navn og lyklafingramerki, áðrenn tú góðkennir skrásetingina.
+          reject: Avvís
+          title: Vátta FASP-skráseting
+        save: Goym
+        select_capabilities: Vel evni
+        sign_in: Rita inn
+        status: Støða
+        title: Fediverse Auxiliary Service Providers
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Viðmæli at fylgja hjálpa nýggjum brúkarum til skjótt at finna áhugavert innihald</strong>. Tá ein brúkari ikki hevur havt samband við aðrar brúkarar til at gera persónlig tilmæli, so verða hesar kontur viðmæltar í staðin. Konturnar verða roknaðar av nýggjum dagliga frá einari blanding av kontum við flestu hugbindingum og flestu lokalu fylgjarum fyri eitt givið mál."
       language: Fyri mál
@@ -939,6 +974,7 @@ fo:
         chance_to_review_html: "<strong>Framleiddu tænastutreytirnar verða ikki útgivnar av sær sjálvum.</strong> Tú fær møguleika at eftirhyggja úrslitini. Vinarliga útfyll neyðugu smálutirnar fyri at halda fram."
         explanation_html: Leisturin við tænastutreytum er einans til kunningar og skal ikki fatast sum løgfrøðislig ráðgeving yvirhøvur. Vinarliga spyr tín egna løgfrøðisliga ráðgeva um tína støðu og ítøkiligu løgfrøðisligu spurningarnar hjá tær.
         title: Uppseting av tænastutreytum
+      going_live_on_html: Galdandi, frá %{date}
       history: Søga
       live: Beinleiðis
       no_history: Enn eru ongar skrásettar broytingar í tænastutreytunum.
@@ -1904,6 +1940,10 @@ fo:
     recovery_instructions_html: Missir tú atgongd til telefonina, so kanst tú brúka eina av kodunum til endurgerð niðanfyri at fáa atgongd aftur til kontu tína. <strong>Goym kodurnar til endurgerð trygt</strong>. Til dømis kanst tú prenta tær og goyma tær saman við øðrum týdningarmiklum skjølum.
     webauthn: Trygdarlyklar
   user_mailer:
+    announcement_published:
+      description: 'Umsitararnir av %{domain} kunngera:'
+      subject: Tænastukunngerð
+      title: "%{domain} tænastukunngerð"
     appeal_approved:
       action: Kontustillingar
       explanation: Kæran um atsóknina móti kontu tínari %{strike_date}, sum tú sendi inn %{appeal_date}, er góðkend. Konta tín er aftur tignarlig.
@@ -1936,8 +1976,8 @@ fo:
     terms_of_service_changed:
       agreement: Við framhaldandi at brúka %{domain} góðtekur tú hesar treytir. Tekur tú ikki undir við dagførdu treytunum, so kanst tú til einhvørja tíð uppsiga avtaluna við %{domain} við at strika kontu tína.
       changelog: 'Í stuttum merkir henda dagføringin:'
-      description: 'Tú móttekur hetta teldubrævið, tí at vit gera nakrar broytingar í okkara tænastutreytum á %{domain}. Vit eggja tær til at eftirhyggja dagførdu treytirnar her:'
-      description_html: Tú móttekur hetta teldubrævið, tí at vit gera nakrar broytingar í okkara tænastutreytum á %{domain}. Vit eggja tær til at eftirhyggja <a href="%{path}" target="_blank">dagførdu og samlaðu treytirnar her</a>.
+      description: 'Tú móttekur hetta teldubrævið, tí at vit gera nakrar broytingar í okkara tænastutreytum á %{domain}. Broytingarnar vera galdandi frá %{date}. Vit eggja tær til at eftirhyggja dagførdu treytirnar her:'
+      description_html: Tú móttekur hetta teldubrævið, tí at vit gera nakrar broytingar í okkara tænastutreytum á %{domain}. Broytingarnar vera galdandi frá <strong>%{date}</strong>. Vit eggja tær til at eftirhyggja <a href="%{path}" target="_blank">dagførdu treytirnar her</a>.
       sign_off: "%{domain} toymið"
       subject: Dagføringar til okkara tænastutreytir
       subtitle: Tænastutreytirnar hjá %{domain} eru við at verða broyttar
diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml
index 37c34b7678..e85952f91c 100644
--- a/config/locales/fr-CA.yml
+++ b/config/locales/fr-CA.yml
@@ -309,6 +309,7 @@ fr-CA:
       title: Journal d’audit
       unavailable_instance: "(nom de domaine indisponible)"
     announcements:
+      back: Retour aux annonces
       destroyed_msg: Annonce supprimée avec succès !
       edit:
         title: Modifier l’annonce
@@ -317,6 +318,9 @@ fr-CA:
       new:
         create: Créer une annonce
         title: Nouvelle annonce
+      preview:
+        explanation_html: 'L''e-mail sera envoyé à <strong>%{display_count} utilisateurs</strong>. Le texte suivant sera inclus :'
+        title: Aperçu de la notification d'annonce
       publish: Publier
       published_msg: Annonce publiée avec succès !
       scheduled_for: Planifiée pour %{time}
@@ -478,6 +482,15 @@ fr-CA:
       new:
         title: Importer des blocages de domaine
       no_file: Aucun fichier sélectionné
+    fasp:
+      providers:
+        registrations:
+          confirm: Confirmer
+          reject: Rejeter
+        save: Enregistrer
+        select_capabilities: Sélectionnez les Capacités
+        sign_in: Se connecter
+        status: État
     follow_recommendations:
       description_html: "<strong>Les recommandations d'abonnement aident les nouvelles personnes à trouver rapidement du contenu intéressant</strong>. Si un·e utilisateur·rice n'a pas assez interagi avec les autres pour avoir des recommandations personnalisées, ces comptes sont alors recommandés. La sélection est mise à jour quotidiennement depuis un mélange de comptes ayant le plus d'interactions récentes et le plus grand nombre d'abonné·e·s locaux pour une langue donnée."
       language: Pour la langue
@@ -942,6 +955,7 @@ fr-CA:
         chance_to_review_html: "<strong>Les conditions d'utilisation générées ne seront pas publiées automatiquement.</strong> Vous aurez la possibilité de vérifier les résultats. Veuillez remplir les informations nécessaires pour continuer."
         explanation_html: Le modèle de conditions d'utilisation fourni l'est uniquement à titre informatif et ne doit pas être interprété comme un conseil juridique sur quelque sujet que ce soit. Veuillez consulter votre propre conseiller juridique sur votre situation et les questions juridiques spécifiques que vous vous posez.
         title: Configuration des Conditions d'Utilisation
+      going_live_on_html: En direct, à compter du %{date}
       history: Historique
       live: En cours d'utilisation
       no_history: Il n'y a pas encore de modifications enregistrées des conditions d'utilisation.
@@ -1907,6 +1921,10 @@ fr-CA:
     recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour retrouver l’accès à votre compte. <strong>Conservez les codes de récupération en sécurité</strong>. Par exemple, en les imprimant et en les stockant avec vos autres documents importants.
     webauthn: Clés de sécurité
   user_mailer:
+    announcement_published:
+      description: 'Les administrateurs de %{domain} font une annonce :'
+      subject: Annonce de service
+      title: Annonce de service de %{domain}
     appeal_approved:
       action: Paramètres du compte
       explanation: L'appel de la sanction contre votre compte mise en place le %{strike_date} que vous avez soumis le %{appeal_date} a été approuvé. Votre compte est de nouveau en règle.
@@ -1939,8 +1957,8 @@ fr-CA:
     terms_of_service_changed:
       agreement: En continuant d'utiliser %{domain}, vous acceptez ces conditions. Si vous n'êtes pas d'accord avec les conditions mises à jour, vous pouvez résilier votre accord avec %{domain} à tout moment en supprimant votre compte.
       changelog: 'En un coup d''œil, voici ce que cette mise à jour signifie pour vous :'
-      description: 'Vous recevez cet e-mail car nous apportons des modifications à nos conditions d''utilisation sur %{domain}. Nous vous encourageons à consulter l''intégralité des conditions mises à jour ici :'
-      description_html: Vous recevez cet e-mail car nous apportons des modifications à nos conditions d'utilisation sur %{domain}. Nous vous encourageons à consulter l'intégralité des <a href="%{path}" target="_blank">conditions mises à jour ici</a>.
+      description: 'Vous recevez cet e-mail parce que nous apportons des modifications à nos conditions de service à %{domain}. Ces modifications entreront en vigueur le %{date}. Nous vous encourageons à consulter l''intégralité des conditions mises à jour ici :'
+      description_html: Vous recevez cet e-mail parce que nous apportons des modifications à nos conditions de service à %{domain}. Ces mises à jour entreront en vigueur le <strong>%{date}</strong>. Nous vous encourageons à consulter l'intégralité des <a href="%{path}" target="_blank">conditions mises à jour ici</a>.
       sign_off: L'équipe %{domain}
       subject: Mises à jour de nos conditions d'utilisation
       subtitle: Les conditions d'utilisation de `%{domain}` changent
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 0c1e248467..25218bd019 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -309,6 +309,7 @@ fr:
       title: Journal d’audit
       unavailable_instance: "(nom de domaine indisponible)"
     announcements:
+      back: Retour aux annonces
       destroyed_msg: Annonce supprimée avec succès !
       edit:
         title: Modifier l’annonce
@@ -317,6 +318,9 @@ fr:
       new:
         create: Créer une annonce
         title: Nouvelle annonce
+      preview:
+        explanation_html: 'L''e-mail sera envoyé à <strong>%{display_count} utilisateurs</strong>. Le texte suivant sera inclus :'
+        title: Aperçu de la notification d'annonce
       publish: Publier
       published_msg: Annonce publiée avec succès !
       scheduled_for: Planifiée pour %{time}
@@ -478,6 +482,15 @@ fr:
       new:
         title: Importer des blocages de domaine
       no_file: Aucun fichier sélectionné
+    fasp:
+      providers:
+        registrations:
+          confirm: Confirmer
+          reject: Rejeter
+        save: Enregistrer
+        select_capabilities: Sélectionnez les Capacités
+        sign_in: Se connecter
+        status: État
     follow_recommendations:
       description_html: "<strong>Les recommandations d'abonnement aident les nouvelles personnes à trouver rapidement du contenu intéressant</strong>. Si un·e utilisateur·rice n'a pas assez interagi avec les autres pour avoir des recommandations personnalisées, ces comptes sont alors recommandés. La sélection est mise à jour quotidiennement depuis un mélange de comptes ayant le plus d'interactions récentes et le plus grand nombre d'abonné·e·s locaux pour une langue donnée."
       language: Pour la langue
@@ -942,6 +955,7 @@ fr:
         chance_to_review_html: "<strong>Les conditions d'utilisation générées ne seront pas publiées automatiquement.</strong> Vous aurez la possibilité de vérifier les résultats. Veuillez remplir les informations nécessaires pour continuer."
         explanation_html: Le modèle de conditions d'utilisation fourni l'est uniquement à titre informatif et ne doit pas être interprété comme un conseil juridique sur quelque sujet que ce soit. Veuillez consulter votre propre conseiller juridique sur votre situation et les questions juridiques spécifiques que vous vous posez.
         title: Configuration des Conditions d'Utilisation
+      going_live_on_html: En direct, à compter du %{date}
       history: Historique
       live: En cours d'utilisation
       no_history: Il n'y a pas encore de modifications enregistrées des conditions d'utilisation.
@@ -1907,6 +1921,10 @@ fr:
     recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour retrouver l’accès à votre compte. <strong>Conservez les codes de récupération en sécurité</strong>. Par exemple, en les imprimant et en les stockant avec vos autres documents importants.
     webauthn: Clés de sécurité
   user_mailer:
+    announcement_published:
+      description: 'Les administrateurs de %{domain} font une annonce :'
+      subject: Annonce de service
+      title: Annonce de service de %{domain}
     appeal_approved:
       action: Paramètres du compte
       explanation: L'appel de la sanction contre votre compte mise en place le %{strike_date} que vous avez soumis le %{appeal_date} a été approuvé. Votre compte est de nouveau en règle.
@@ -1939,8 +1957,8 @@ fr:
     terms_of_service_changed:
       agreement: En continuant d'utiliser %{domain}, vous acceptez ces conditions. Si vous n'êtes pas d'accord avec les conditions mises à jour, vous pouvez résilier votre accord avec %{domain} à tout moment en supprimant votre compte.
       changelog: 'En un coup d''œil, voici ce que cette mise à jour signifie pour vous :'
-      description: 'Vous recevez cet e-mail car nous apportons des modifications à nos conditions d''utilisation sur %{domain}. Nous vous encourageons à consulter l''intégralité des conditions mises à jour ici :'
-      description_html: Vous recevez cet e-mail car nous apportons des modifications à nos conditions d'utilisation sur %{domain}. Nous vous encourageons à consulter l'intégralité des <a href="%{path}" target="_blank">conditions mises à jour ici</a>.
+      description: 'Vous recevez cet e-mail parce que nous apportons des modifications à nos conditions de service à %{domain}. Ces modifications entreront en vigueur le %{date}. Nous vous encourageons à consulter l''intégralité des conditions mises à jour ici :'
+      description_html: Vous recevez cet e-mail parce que nous apportons des modifications à nos conditions de service à %{domain}. Ces mises à jour entreront en vigueur le <strong>%{date}</strong>. Nous vous encourageons à consulter l'intégralité des <a href="%{path}" target="_blank">conditions mises à jour ici</a>.
       sign_off: L'équipe %{domain}
       subject: Mises à jour de nos conditions d'utilisation
       subtitle: Les conditions d'utilisation de `%{domain}` changent
diff --git a/config/locales/fy.yml b/config/locales/fy.yml
index a1843a111e..119d08be2d 100644
--- a/config/locales/fy.yml
+++ b/config/locales/fy.yml
@@ -1936,8 +1936,6 @@ fy:
     terms_of_service_changed:
       agreement: Troch %{domain} brûke te bliuwen, geane jo akkoard mei dizze betingsten. As jo it net iens binne mei de bywurke betingsten, kinne jo jo oerienkomst mei %{domain} op elk winske momint beëinigje troch jo account fuort te smiten.
       changelog: 'Yn ien eachopslach betsjut dizze update foar jo:'
-      description: 'Jo ûntfange dit berjocht, omdat wy inkelde wizigingen oanbringe yn ús gebrûksbetingsten by %{domain}. Wy riede jo oan om de bywurke betingsten hjir folslein te besjen:'
-      description_html: Jo ûntfange dit berjocht, omdat wy inkelde wizigingen oanbringe yn ús gebrûksbetingsten by %{domain}. Wy riede jo oan om de bywurke <a href="%{path}" target="_blank">betingsten hjir folslein te besjen</a>.
       sign_off: It %{domain}-team
       subject: Aktualisaasje fan ús tsjinstbetingsten
       subtitle: De gebrûksbetingsten fan %{domain} wizigje
diff --git a/config/locales/ga.yml b/config/locales/ga.yml
index 56f66609c3..58d02fc6a9 100644
--- a/config/locales/ga.yml
+++ b/config/locales/ga.yml
@@ -318,6 +318,7 @@ ga:
       title: Loga iniúchta
       unavailable_instance: "(ainm fearainn ar fáil)"
     announcements:
+      back: Ar ais chuig fógraí
       destroyed_msg: D'éirigh leis an bhfógra a scriosadh!
       edit:
         title: Cuir fógra in eagar
@@ -326,6 +327,9 @@ ga:
       new:
         create: Cruthaigh fógra
         title: Fógra nua
+      preview:
+        explanation_html: 'Seolfar an ríomhphost chuig <strong>%{display_count} úsáideoir</strong>. Beidh an téacs seo a leanas san áireamh sa ríomhphost:'
+        title: Fógra réamhamhairc na fógraíochta
       publish: Foilsigh
       published_msg: D’éirigh leis an bhfógra a fhoilsiú!
       scheduled_for: Sceidealta le haghaidh %{time}
@@ -499,6 +503,36 @@ ga:
       new:
         title: Iompórtáil bloic fearainn
       no_file: Níor roghnaíodh aon chomhad
+    fasp:
+      debug:
+        callbacks:
+          created_at: Cruthaithe ag
+          delete: Scrios
+          ip: Seoladh IP
+          request_body: Comhlacht iarratais
+          title: Glaonna Dífhabhtaithe
+      providers:
+        active: Gníomhach
+        base_url: Bun-URL
+        callback: Glao ar ais
+        delete: Scrios
+        edit: Cuir Soláthraí in Eagar
+        finish_registration: Críochnaigh clárú
+        name: Ainm
+        providers: Soláthraithe
+        public_key_fingerprint: Méarloirg eochair phoiblí
+        registration_requested: Clárú iarrtha
+        registrations:
+          confirm: Deimhnigh
+          description: Fuair ​​tú clárú ó FASP. Diúltaigh é murar chuir tú tús leis seo. Má thionscain tú é seo, déan comparáid chúramach idir an t-ainm agus an eochair-mhéarlorg sula ndearbhaítear an clárúchán.
+          reject: Diúltaigh
+          title: Deimhnigh Clárú FASP
+        save: Sábháil
+        select_capabilities: Roghnaigh Cumais
+        sign_in: Sínigh Isteach
+        status: Stádas
+        title: Soláthraithe Seirbhíse Cúnta Fediverse
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Lean na moltaí cabhraíonn sé le húsáideoirí nua ábhar suimiúil a aimsiú go tapa</strong>. Nuair nach mbíonn go leor idirghníomhaithe ag úsáideoir le daoine eile chun moltaí pearsantaithe a leanúint, moltar na cuntais seo ina ionad sin. Déantar iad a athríomh ar bhonn laethúil ó mheascán de chuntais a bhfuil na rannpháirtíochtaí is airde acu le déanaí agus na háirimh áitiúla is airde leanúna do theanga ar leith."
       language: Don teanga
@@ -981,6 +1015,7 @@ ga:
         chance_to_review_html: "<strong>Ní fhoilseofar na téarmaí seirbhíse ginte go huathoibríoch.</strong> Beidh deis agat na torthaí a athbhreithniú. Líon isteach na sonraí riachtanacha le leanúint ar aghaidh."
         explanation_html: Is chun críocha faisnéise amháin atá an teimpléad téarmaí seirbhíse a chuirtear ar fáil, agus níor cheart é a fhorléiriú mar chomhairle dlí ar aon ábhar. Téigh i gcomhairle le do chomhairle dlí féin maidir le do chás agus ceisteanna dlí ar leith atá agat.
         title: Téarmaí Socrú Seirbhíse
+      going_live_on_html: Beo, éifeachtach %{date}
       history: Stair
       live: Beo
       no_history: Níl aon athruithe taifeadta ar théarmaí seirbhíse fós.
@@ -1266,6 +1301,7 @@ ga:
     too_fast: Cuireadh an fhoirm isteach róthapa, triail arís.
     use_security_key: Úsáid eochair shlándála
     user_agreement_html: Léigh mé agus aontaím leis na <a href="%{terms_of_service_path}" target="_blank">téarmaí seirbhíse</a> agus <a href="%{privacy_policy_path}" target="_blank">polasaí príobháideachais</a>
+    user_privacy_agreement_html: Léigh mé agus aontaím leis an <a href="%{privacy_policy_path}" target="_blank">polasaí príobháideachais</a>
   author_attribution:
     example_title: Téacs samplach
     hint_html: An bhfuil tú ag scríobh altanna nuachta nó blag lasmuigh de Mastodon? Rialú conas a gheobhaidh tú creidmheas nuair a roinntear iad ar Mastodon.
@@ -2032,6 +2068,10 @@ ga:
     recovery_instructions_html: Má chailleann tú rochtain ar do ghuthán riamh, is féidir leat ceann de na cóid athshlánaithe thíos a úsáid chun rochtain a fháil ar do chuntas arís. <strong>Coinnigh na cóid athshlánaithe slán</strong>. Mar shampla, is féidir leat iad a phriontáil agus iad a stóráil le doiciméid thábhachtacha eile.
     webauthn: Eochracha slándála
   user_mailer:
+    announcement_published:
+      description: 'Tá riarthóirí %{domain} ag déanamh fógra:'
+      subject: Fógra seirbhíse
+      title: "%{domain} fógra seirbhíse"
     appeal_approved:
       action: Socruithe cuntas
       explanation: Ceadaíodh achomharc na stailce i gcoinne do chuntais ar %{strike_date} a chuir tú isteach ar %{appeal_date}. Tá seasamh maith ag do chuntas arís.
@@ -2064,8 +2104,8 @@ ga:
     terms_of_service_changed:
       agreement: Má leanann tú ar aghaidh ag úsáid %{domain}, tá tú ag aontú leis na téarmaí seo. Mura n-aontaíonn tú leis na téarmaí nuashonraithe, is féidir leat do chomhaontú le %{domain} a fhoirceannadh am ar bith trí do chuntas a scriosadh.
       changelog: 'Sracfhéachaint, seo é a chiallaíonn an nuashonrú seo duit:'
-      description: 'Tá an ríomhphost seo á fháil agat toisc go bhfuil roinnt athruithe á ndéanamh againn ar ár dtéarmaí seirbhíse ag %{domain}. Molaimid duit athbhreithniú iomlán a dhéanamh ar na téarmaí nuashonraithe anseo:'
-      description_html: Tá an ríomhphost seo á fháil agat toisc go bhfuil roinnt athruithe á ndéanamh againn ar ár dtéarmaí seirbhíse ag %{domain}. Molaimid duit athbhreithniú a dhéanamh ar na <a href="%{path}" target="_blank">téarmaí nuashonraithe ina n-iomláine anseo</a>.
+      description: 'Tá an ríomhphost seo á fháil agat toisc go bhfuil roinnt athruithe á ndéanamh againn ar ár dtéarmaí seirbhíse ag %{domain}. Beidh na nuashonruithe seo i bhfeidhm ar %{date}. Molaimid duit athbhreithniú iomlán a dhéanamh ar na téarmaí nuashonraithe anseo:'
+      description_html: Tá an ríomhphost seo á fháil agat toisc go bhfuil roinnt athruithe á ndéanamh againn ar ár dtéarmaí seirbhíse ag %{domain}. Tiocfaidh na nuashonruithe seo i bhfeidhm ar <strong>%{date}</strong>. Molaimid duit athbhreithniú a dhéanamh ar na <a href="%{path}" target="_blank">téarmaí nuashonraithe ina n-iomláine anseo</a>.
       sign_off: Foireann %{domain}
       subject: Nuashonruithe ar ár dtéarmaí seirbhíse
       subtitle: Tá téarmaí seirbhíse %{domain} ag athrú
diff --git a/config/locales/gd.yml b/config/locales/gd.yml
index 850143567f..b9b40ac8b5 100644
--- a/config/locales/gd.yml
+++ b/config/locales/gd.yml
@@ -1830,7 +1830,7 @@ gd:
       user_domain_block: Bhac thu %{target_name}
     lost_followers: An luchd-leantainn a chaill thu
     lost_follows: Daoine nach lean thu tuilleadh
-    preamble: Dh’fhaoidte gun chaill thu dàimhean leantainn nuair a bhacas tu àrainn no nuair a chuireas na maoir romhpa gun cuir iad frithealaiche cèin à rèim. Nuair a thachras sin, ’s urrainn dhut liosta de na dàimhean dealaichte a luchdadh a-nuas airson sùil a thoirt orra agus an ion-phortadh gu frithealaiche eile ’s dòcha.
+    preamble: Dh’fhaoidte gun caill thu dàimhean leantainn nuair a bhacas tu àrainn no nuair a chuireas na maoir romhpa gun cuir iad frithealaiche cèin à rèim. Nuair a thachras sin, ’s urrainn dhut liosta de na dàimhean dealaichte a luchdadh a-nuas airson sùil a thoirt orra agus an ion-phortadh gu frithealaiche eile ’s dòcha.
     purged: Chaidh am fiosrachadh mun fhrithealaiche seo a phurgaideachadh le rianairean an fhrithealaiche agad.
     type: Tachartas
   statuses:
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index 1db9b74b69..ed51799e8a 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -271,11 +271,11 @@ gl:
         disable_2fa_user_html: "%{name} desactivou o requerimento do segundo factor para a usuaria %{target}"
         disable_custom_emoji_html: "%{name} desactivou o emoji %{target}"
         disable_relay_html: "%{name} desactivou o repetidor %{target}"
-        disable_sign_in_token_auth_user_html: "%{name} desactivou a autenticación por token no email para %{target}"
+        disable_sign_in_token_auth_user_html: "%{name} desactivou a autenticación por token no correo para %{target}"
         disable_user_html: "%{name} desactivou as credenciais para a usuaria %{target}"
         enable_custom_emoji_html: "%{name} activou o emoji %{target}"
         enable_relay_html: "%{name} activou o repetidor %{target}"
-        enable_sign_in_token_auth_user_html: "%{name} activou a autenticación con token no email para %{target}"
+        enable_sign_in_token_auth_user_html: "%{name} activou a autenticación con token no correo para %{target}"
         enable_user_html: "%{name} activou as credenciais para a usuaria %{target}"
         memorialize_account_html: "%{name} convertiu a conta de %{target} nunha páxina para o recordo"
         promote_user_html: "%{name} promocionou a usuaria %{target}"
@@ -309,6 +309,7 @@ gl:
       title: Rexistro de auditoría
       unavailable_instance: "(nome de dominio non dispoñible)"
     announcements:
+      back: Anuncios anteriores
       destroyed_msg: Anuncio eliminado de xeito correcto!
       edit:
         title: Editar anuncio
@@ -317,6 +318,10 @@ gl:
       new:
         create: Crear anuncio
         title: Novo anuncio
+      preview:
+        disclaimer: As usuarias non poden omitilas, as notificiacións por correo deberían limitarse a anuncios importantes como fugas de datos personais ou notificación do cese do servizo.
+        explanation_html: 'Vaise enviar o correo a <strong>%{display_count} usuarias</strong>. Incluirase o seguinte texto no correo:'
+        title: Previsualización da notificación do anuncio
       publish: Publicar
       published_msg: Anuncio publicado de xeito correcto!
       scheduled_for: Programado para %{time}
@@ -475,6 +480,36 @@ gl:
       new:
         title: Importar bloqueos de dominio
       no_file: Ningún ficheiro seleccionado
+    fasp:
+      debug:
+        callbacks:
+          created_at: Creado o
+          delete: Eliminar
+          ip: Enderezo IP
+          request_body: Corpo da solicitude
+          title: Datos para depuración
+      providers:
+        active: Activo
+        base_url: URL base
+        callback: Resposta
+        delete: Eliminar
+        edit: Editar provedor
+        finish_registration: Finalizar rexistro
+        name: Nome
+        providers: Provedores
+        public_key_fingerprint: Impresión dixital da chave pública
+        registration_requested: Solicitouse o rexistro
+        registrations:
+          confirm: Confirmar
+          description: Recibiches unha solicitude de rexistro desde un FASP. Rexeitaa se non iniciaches ti o proceso. Se foi iniciada por ti, compara con atención o nome e impresión dixital antes de confirmar o rexistro.
+          reject: Rexeitar
+          title: Confirmar o rexistro do FASP
+        save: Gardar
+        select_capabilities: Seleccionar funcións
+        sign_in: Acceder
+        status: Estado
+        title: Provedores de Servizos Auxiliares do Fediverso
+      title: FASP
     follow_recommendations:
       description_html: "<strong>As recomendacións de seguimento son útiles para que as novas usuarias atopen contidos interesantes</strong>. Cando unha usuaria aínda non interactuou con outras para obter recomendacións de seguimento, estas contas serán recomendadas. Variarán a diario xa que se escollen en base ao maior número de interaccións e ao contador local de seguimentos para un idioma dado."
       language: Para o idioma
@@ -939,6 +974,7 @@ gl:
         chance_to_review_html: "<strong>Vanse publicar automaticamente os termos do servizo creados.</strong> Terás a oportunidade de revisar o resultado. Por favor completa os detalles precisos para continuar."
         explanation_html: O modelo dos termos do servizo proporcionados é soamente informativo, e en ningún caso constitúe un consello legal. Por favor realiza unha consulta legal sobre a túa situación concreta e posibles cuestións legais que debas afrontar.
         title: Configurar os Termos do Servizo
+      going_live_on_html: Aplicados, con efecto desde %{date}
       history: Historial
       live: Actuais
       no_history: Non hai rexistrados cambios nos termos do servizo.
@@ -1298,7 +1334,7 @@ gl:
   errors:
     '400': A solicitude que enviou non é válida ou ten formato incorrecto.
     '403': Non ten permiso para ver esta páxina.
-    '404': A páxina que está a buscar non está aquí.
+    '404': A páxina que buscas non está aquí.
     '406': Esta páxina non está dispoñible no formato solicitado.
     '410': A páxina que estaba a buscar xa non existe.
     '422':
@@ -1904,6 +1940,10 @@ gl:
     recovery_instructions_html: Se perdeses o acceso ao teu teléfono, podes utilizar un dos códigos de recuperación inferiores para recuperar o acceso á conta. <strong>Garda os códigos nun lugar seguro</strong>. Por exemplo, podes imprimilos e gardalos xunto con outros documentos importantes.
     webauthn: Chaves de seguridade
   user_mailer:
+    announcement_published:
+      description: A administración de %{domain} publicou un anuncio
+      subject: Información de servizo
+      title: Información de servizo de %{domain}
     appeal_approved:
       action: Axustes da conta
       explanation: A apelación da acción contra a túa conta o %{strike_date} que enviaches o %{appeal_date} foi aprobada. A túa conta volve ao estado normal de uso.
@@ -1936,8 +1976,8 @@ gl:
     terms_of_service_changed:
       agreement: Se continúas a usar %{domain} aceptas estas condicións. Se non aceptas as condicións actualizadas podería rematar o acordo con %{domain} en calquera momento e eliminarse a túa conta.
       changelog: 'Dunha ollada, aquí tes o que implican os cambios para ti:'
-      description: 'Recibes este correo porque fixemos cambios nos termos do servizo de %{domain}. Recomendámosche que leas as condicións actualizadas ao completo aquí:'
-      description_html: Recibes este correo porque fixemos cambios nos termos do servizo de %{domain}. Recomendámosche que leas as <a href="%{path}" target="_blank">condicións actualizadas ao completo aquí</a>.
+      description: 'Estás a recibir este correo porque fixemos cambios nos termos do servizo en %{domain}. Estas actualizacións terán efecto desde o %{date}. Convidámoste a que revises os termos ao completo aquí:'
+      description_html: 'Estás a recibir este correo porque fixemos algúns cambios nos nosos termos do servizo en %{domain}. Estas actualizacións terán efecto desde o <strong>%{date}</strong>. Convidámoste a que leas <a href="%{path}" target="_blank">aquí as condicións actualizadas ao completo</a>:'
       sign_off: O equipo de %{domain}
       subject: Actualización dos nosos termos do servizo
       subtitle: Cambiaron os termos do servizo de %{domain}
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 0a68d338f0..457569d05a 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -232,7 +232,7 @@ he:
         silence_account: הגבלת חשבון
         suspend_account: השעיית חשבון
         unassigned_report: ביטול הקצאת דו"ח
-        unblock_email_account: ביטול חסימת כתובת דוא"ל
+        unblock_email_account: הסרת חסימת כתובת דוא"ל
         unsensitive_account: ביטול Force-Sensitive לחשבון
         unsilence_account: ביטול השתקת חשבון
         unsuspend_account: ביטול השעיית חשבון
@@ -263,11 +263,11 @@ he:
         create_user_role_html: "%{name} יצר את התפקיד של %{target}"
         demote_user_html: "%{name} הוריד/ה בדרגה את המשתמש %{target}"
         destroy_announcement_html: "%{name} מחק/ה את ההכרזה %{target}"
-        destroy_canonical_email_block_html: "%{name} הסיר/ה חסימה מדואל %{target}"
+        destroy_canonical_email_block_html: "%{name} הסירו חסימה מדואל %{target}"
         destroy_custom_emoji_html: "%{name} מחק אמוג'י של %{target}"
         destroy_domain_allow_html: "%{name} לא התיר/ה פדרציה עם הדומיין %{target}"
-        destroy_domain_block_html: "%{name} הסיר/ה חסימה מהדומיין %{target}"
-        destroy_email_domain_block_html: '%{name} הסיר/ה חסימה מדומיין הדוא"ל %{target}'
+        destroy_domain_block_html: החסימה על מתחם %{target} הוסרה ע"י %{name}
+        destroy_email_domain_block_html: הוסרה חסימת מתחם דוא"ל %{target} בידי %{name}
         destroy_instance_html: "%{name} טיהר/ה את הדומיין %{target}"
         destroy_ip_block_html: "%{name} מחק/ה את הכלל עבור IP %{target}"
         destroy_relay_html: "%{name} מחקו את הממסר %{target}"
@@ -315,6 +315,7 @@ he:
       title: ביקורת יומן
       unavailable_instance: "(שם מתחם לא זמין)"
     announcements:
+      back: חזרה להודעות
       destroyed_msg: הכרזה נמחקה בהצלחה!
       edit:
         title: עריכת הכרזה
@@ -323,6 +324,10 @@ he:
       new:
         create: יצירת הכרזה
         title: הכרזה חדשה
+      preview:
+        disclaimer: כיוון שהמשתמשים לא יכולים לבטל אותם, הודעות דוא"ל צריכות להיות מוגבלות בשימוש להודעות חשובות כגון הודעות על גניבת מידע אישי או הודעות על סגירת השרת.
+        explanation_html: 'הדואל ישלח אל <strong>%{display_count} משתמשיםות</strong>. להלן המלל שישלח בדואל:'
+        title: צפיה מקדימה בהודעה
       publish: פרסום
       published_msg: ההכרזה פורסמה בהצלחה!
       scheduled_for: מתוזמן ל-%{time}
@@ -491,6 +496,36 @@ he:
       new:
         title: יבוא רשימת שרתים חסומים
       no_file: לא נבחר קובץ
+    fasp:
+      debug:
+        callbacks:
+          created_at: תאריך יצירה
+          delete: מחיקה
+          ip: כתובת IP
+          request_body: גוף הבקשה
+          title: ניפוי תקלות בקריאות חוזרות
+      providers:
+        active: פעילים
+        base_url: קישור בסיס
+        callback: קריאה חוזרת
+        delete: מחיקה
+        edit: עריכת ספק
+        finish_registration: סיום הרשמה
+        name: שם
+        providers: ספקים
+        public_key_fingerprint: טביעת האצבע של המפתח הציבורי
+        registration_requested: נדרשת הרשמה
+        registrations:
+          confirm: אישור
+          description: קיבלת הרשמה דרך FASP. יש לדחות אותה אם לא ביקשת את ההרשמה הזו מיוזמתך. אם זו בקשה מיוזמתך, יש להשוות בהקפדה אם השם וטביעת האצבע של המפתח הציבורי תואמים לפני אישור הרישום.
+          reject: דחיה
+          title: אישור הרשמת FASP
+        save: שמירה
+        select_capabilities: בחירת יכולות
+        sign_in: כניסה
+        status: מצב
+        title: ספקי משנה לפדיוורס
+      title: פרוטוקול FASP
     follow_recommendations:
       description_html: "<strong>עקבו אחר ההמלצות על מנת לעזור למשתמשים חדשים למצוא תוכן מעניין</strong>. במידה ומשתמש לא תקשר מספיק עם משתמשים אחרים כדי ליצור המלצות מעקב, חשבונות אלה יומלצו במקום. הם מחושבים מחדש על בסיסי יומיומי מתערובת של החשבונות הפעילים ביותר עם החשבונות הנעקבים ביותר עבור שפה נתונה."
       language: עבור שפה
@@ -967,6 +1002,7 @@ he:
         chance_to_review_html: "<strong>תנאי השירות שחוללו עצמונית לא יפורסמו אוטומטית.</strong> תהיה לך הזדמנות לעבור על התוצאה. יש למלא את הפרטים הבאים כדי להמשיך."
         explanation_html: תבנית תנאי השירות סופקה לצרכי יידוע בלבד, ואין לראות בהם עצה חוקית על אף נושא. אנא התייעצו בעצמבם עם פרקליט לגבי מצבכם הייחודי ושאלות ספציפיות שעלולות להיות לכם.
         title: הקמת מסמך תנאי השירות
+      going_live_on_html: בתוקף מתאריך %{date}
       history: גרסאות העבר
       live: הגרסא החיה
       no_history: עוד לא נרשמו שינויים בתנאי השירות.
@@ -1990,6 +2026,10 @@ he:
     recovery_instructions_html: במידה והגישה למכשירך תאבד, ניתן לייצר קודי אחזור למטה על מנת לאחזר גישה לחשבונך בכל עת. <strong>נא לשמור על קודי הגישה במקום בטוח</strong>. לדוגמא על ידי הדפסתם ושמירתם עם מסמכים חשובים אחרים, או שימוש בתוכנה ייעודית לניהול סיסמאות וסודות.
     webauthn: מפתחות אבטחה
   user_mailer:
+    announcement_published:
+      description: 'צוות ההנהלה של %{domain} מפרסם הודעה:'
+      subject: הודעת שירות
+      title: הודעת שירות מאת %{domain}
     appeal_approved:
       action: הגדרות חשבון
       explanation: הערעור על העברה שנרשמה כנגד חשבונך ב-%{strike_date} שהגשת ב-%{appeal_date} התקבל. חשבונך חזר להיות נקי מכל רבב.
@@ -2022,8 +2062,8 @@ he:
     terms_of_service_changed:
       agreement: עם המשך השימוש בשרת %{domain} אתן מסכימות לתנאים הללו. אם אינכם מסכימים עם עדכוני תנאי השירות, אתן יכולות להפסיק את ההסכם עם %{domain} בכל עת על ידי מחיקת החשבון.
       changelog: 'בקצרה, הנה משמעות העדכון עבורך:'
-      description: 'קיבלת הודעת דואל זו כיוון שאנו מבצעים שינויים במסמך תנאי השירות של %{domain}. אנו מעודדים אותך לעבור על השינויים במסמך המלא כאן:'
-      description_html: 'קיבלת הודעת דואל זו כיוון שאנו מבצעים שינויים במסמך תנאי השירות של %{domain}. אנו מעודדים אותך לעבור על <a href="%{path}" target="_blank">השינויים במסמך המלא כאן</a>:'
+      description: 'קיבלת הודעת דואל זו כיוון שאנו מבצעים שינויים במסמך תנאי השירות של %{domain}. השינויים יכנסו לתוקף בתאריך %{date}. אנו מעודדים אותך לעבור על השינויים במסמך המלא כאן:'
+      description_html: קיבלת הודעת דואל זו כיוון שאנו מבצעים שינויים במסמך תנאי השירות של %{domain}. השינויים יכנסו לתוקף בתאריך <strong>%{date}</strong>. אנו מעודדים אותך לעבור על <a href="%{path}" target="_blank">השינויים במסמך המלא כאן</a>.
       sign_off: צוות %{domain}
       subject: עדכונים לתנאי השירות שלנו
       subtitle: מסמך תנאי השירות של %{domain} עוברים שינויים
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 722c361710..ef5d8d97e6 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -309,6 +309,7 @@ hu:
       title: Audit napló
       unavailable_instance: "(domain név nem elérhető)"
     announcements:
+      back: Vissza a bejelentésekhez
       destroyed_msg: A közlemény sikeresen törölve!
       edit:
         title: Közlemény szerkesztése
@@ -317,6 +318,10 @@ hu:
       new:
         create: Közlemény létrehozása
         title: Új közlemény
+      preview:
+        disclaimer: Mivel a felhasználók nem iratkozhatnak le róluk, az e-mailes értesítéseket érdemes a fontos bejelentésekre korlátozni, mint a személyes adatokat érintő adatvédelmi incidensek vagy a kiszolgáló bezárására vonatkozó értesítések.
+        explanation_html: 'Az e-mail <strong>%{display_count} felhasználónak</strong> lesz elküldve. A következő szöveg fog szerepelni a levélben:'
+        title: Közleményértesítés előnézete
       publish: Közzététel
       published_msg: A közlemény sikeresen publikálva!
       scheduled_for: Ekkorra ütemezve %{time}
@@ -475,6 +480,36 @@ hu:
       new:
         title: Domain tiltások importálása
       no_file: Nincs fájl kiválasztva
+    fasp:
+      debug:
+        callbacks:
+          created_at: Létrehozva
+          delete: Törlés
+          ip: IP-cím
+          request_body: Kérés törzse
+          title: Hibakeresési visszahívások
+      providers:
+        active: Aktív
+        base_url: Alapwebcím
+        callback: Visszahívás
+        delete: Törlés
+        edit: Szolgáltató szerkesztése
+        finish_registration: Regisztráció befejezése
+        name: Név
+        providers: Szolgáltatók
+        public_key_fingerprint: Nyilvános kulcs ujjlenyomata
+        registration_requested: Regisztráció kérve
+        registrations:
+          confirm: Megerősítés
+          description: Regisztrációt kaptál egy FASP-től. Ha nem te kérted, utasítsd el. Ha te kérted, akkor a regisztráció megerősítése előtt figyelmesen hasonlítsd össze a nevét és a kulcsának ujjlenyomatát.
+          reject: Elutasítás
+          title: FASP regisztráció megerősítése
+        save: Mentés
+        select_capabilities: Képességek kiválasztása
+        sign_in: Bejelentkezés
+        status: Állapot
+        title: A Födiverzum segédszolgáltatói
+      title: FASP
     follow_recommendations:
       description_html: "<strong>A követési ajánlatok segítik az új felhasználókat az érdekes tartalmak gyors megtalálásában</strong>. Ha egy felhasználó még nem érintkezett eleget másokkal ahhoz, hogy személyre szabott ajánlatokat kapjon, ezeket a fiókokat ajánljuk helyette. Ezeket naponta újraszámítjuk a nemrég legtöbb embert foglalkoztató, illetve legtöbb helyi követővel rendelkező fiókok alapján."
       language: Ezen a nyelven
@@ -939,6 +974,7 @@ hu:
         chance_to_review_html: "<strong>Az előállított felhasználási feltételek nem lesznek automatikusan közzétéve.</strong> Előtte áttekintheted az eredményt. A folytatáshoz töltsd ki a szükséges részleteket."
         explanation_html: A felhasználási feltételek sablonja csak információs célokat szolgál, és semmilyen témában nem tekinthető jogi tanácsadásnak. Konzultálj a saját jogi tanácsadóddal a helyzetedről és a felmerülő konkrét jogi kérdésekről.
         title: Felhasználási feltételek beállítása
+      going_live_on_html: 'Éles, hatálybalépés dátuma: %{date}'
       history: Előzmények
       live: Élő
       no_history: Még nincsenek változtatások a szolgáltatási feltételekben.
@@ -1904,6 +1940,10 @@ hu:
     recovery_instructions_html: A visszaállítási kódok egyikének segítségével tudsz majd belépni, ha elveszítenéd a telefonod. <strong>Tartsd biztos helyen a visszaállítási kódjaid</strong>! Például nyomtasd ki őket és tárold a többi fontos iratoddal együtt.
     webauthn: Biztonsági kulcsok
   user_mailer:
+    announcement_published:
+      description: 'A(z) %{domain} adminisztrátorai a következő bejelentést teszik:'
+      subject: Szolgáltatási közlemény
+      title: A(z) %{domain} szolgáltatási közleménye
     appeal_approved:
       action: Fiók Beállításai
       explanation: A fiókod %{appeal_date}-i fellebbezése, mely a %{strike_date}-i vétségeddel kapcsolatos, jóváhagyásra került. A fiókod megint makulátlan.
@@ -1936,8 +1976,8 @@ hu:
     terms_of_service_changed:
       agreement: A(z) %{domain} használatának folytatásával beleegyezel ezekbe a feltételekbe. Ha nem értesz egyet a frissített feltételekkel, akkor a fiókod törlésével megszakíthatod a(z) %{domain} weboldallal való megállapodásodat.
       changelog: 'Dióhéjban ez a frissítés ezt jelenti számodra:'
-      description: 'Azért kapod ezt az e-mailt, mert a %{domain} felhasználási feltételein változtatunk. Javasoljuk, hogy tekintsd át a frissített feltételeket teljes egészében itt:'
-      description_html: Azért kapod ezt az e-mailt, mert a %{domain} felhasználási feltételein változtatunk. Javasoljuk, hogy tekintsd át a <a href="%{path}" target="_blank">frissített feltételeket teljes egészében itt</a>.
+      description: 'Azért kapod ezt az e-mailt, mert változtatunk a(z) %{domain} felhasználási feltételein. A változások ekkor lépnek életbe: %{date}. Javasoljuk, hogy tekintsd át a frissített feltételeket teljes egészében itt:'
+      description_html: 'Azért kapod ezt az e-mailt, mert változtatunk a(z) %{domain} felhasználási feltételein. A változások ekkor lépnek életbe: <strong>%{date}</strong>. Javasoljuk, hogy tekintsd át a <a href="%{path}" target="_blank">frissített feltételeket teljes egészében itt</a>.'
       sign_off: A(z) %{domain} csapata
       subject: A felhasználási feltételei frissítései
       subtitle: A(z) %{domain} felhasználási feltételei megváltoznak
diff --git a/config/locales/ia.yml b/config/locales/ia.yml
index f42271d16c..760bddb4d7 100644
--- a/config/locales/ia.yml
+++ b/config/locales/ia.yml
@@ -1209,6 +1209,7 @@ ia:
     too_fast: Formulario inviate troppo rapidemente. Tenta lo de novo.
     use_security_key: Usar clave de securitate
     user_agreement_html: Io ha legite e accepta le <a href="%{terms_of_service_path}" target="_blank">conditiones de servicio</a> e le <a href="%{privacy_policy_path}" target="_blank">politica de confidentialitate</a>
+    user_privacy_agreement_html: Io ha legite e io accepta le <a href="%{privacy_policy_path}" target="_blank">politica de confidentialitate</a>
   author_attribution:
     example_title: Texto de exemplo
     hint_html: Scribe tu articulos de novas o de blog foras de Mastodon? Controla le maniera in que tu recipe attribution quando on los condivide sur Mastodon.
@@ -1656,7 +1657,7 @@ ia:
     posting_defaults: Parametros de publication predefinite
     public_timelines: Chronologias public
   privacy:
-    hint_html: "<strong>Personalisa como tu vole que tu profilo e tu messages es trovate.</strong> Un varietate de functiones in Mastodon pote adjutar te a attinger un plus grande publico quando activate. Prende un momento pro revider iste parametros pro assecurar te que illos se adapta a tu besonios."
+    hint_html: "<strong>Personalisa como tu vole que le gente trova tu profilo e tu messages.</strong> Un varietate de functiones in Mastodon pote adjutar te a attinger un plus grande publico si tu los activa. Prende un momento pro revider iste parametros pro assecurar te que illos se adapta a tu besonios."
     privacy: Confidentialitate
     privacy_hint_html: Controla quanto tu vole divulgar pro le beneficio de alteres. Le gente discoperi profilos e applicationes interessante percurrente le profilos sequite per altere personas e vidente a partir de qual applicationes illos publica lor messages, ma tu pote preferer de mantener tal information private.
     reach: Portata
@@ -1935,8 +1936,6 @@ ia:
     terms_of_service_changed:
       agreement: Si tu continua a usar %{domain}, tu accepta iste conditiones. Si tu non es de accordo con le conditiones actualisate, tu pote sempre eliminar tu conto pro terminar tu accordo con %{domain}.
       changelog: 'In summario, ecce lo que iste actualisation significa pro te:'
-      description: 'Tu recipe iste message perque nos ha apportate alcun modificationes a nostre conditiones de servicio sur %{domain}. Nos te incoragia a revider le conditiones actualisate complete al sequente adresse:'
-      description_html: Tu recipe iste message perque nos ha apportate alcun modificationes a nostre conditiones de servicio sur %{domain}. Nos te incoragia a revider le <a href="%{path}" target="_blank">conditiones actualisate complete</a>.
       sign_off: Le equipa de %{domain}
       subject: Actualisationes de nostre conditiones de servicio
       subtitle: Le conditiones de servicio de %{domain} ha cambiate
diff --git a/config/locales/io.yml b/config/locales/io.yml
index c89d920f30..ee905aaf86 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -21,15 +21,19 @@ io:
       one: Posto
       other: Posti
     posts_tab_heading: Posti
+    self_follow_error: Sequar vua sua konto es ne permisita
   admin:
     account_actions:
       action: Agez
+      already_silenced: Ca konto ja limitigesis.
+      already_suspended: Ca konto ja blokusesis.
       title: Agez jero a %{acct}
     account_moderation_notes:
       create: Pozez noto
       created_msg: Jernoto sucesoze kreesis!
       destroyed_msg: Jernoto sucesoze efacesis!
     accounts:
+      add_email_domain_block: Blokusar retpostodomeno
       approve: Aprobez
       approved_msg: Sucesoze aprobis registroapliko di %{username}
       are_you_sure: Ka tu esas certa?
@@ -44,6 +48,7 @@ io:
         title: Chanjez retposto por %{username}
       change_role:
         changed_msg: Rolo sucesoze chanjesis!
+        edit_roles: Administrar uzantoroli
         label: Chanjez rolo
         no_role: Nula rolo
         title: Chanjez rolo por %{username}
@@ -56,31 +61,33 @@ io:
       demote: Despromocez
       destroyed_msg: Informi di %{username} nun aranjesis por efacesar aparante
       disable: Frostigez
-      disable_two_factor_authentication: Desaktivigez 2FA
+      disable_sign_in_token_auth: Desebligar retpostofichyurizo
+      disable_two_factor_authentication: Desebligar 2FA
       disabled: Desinterdiktita
       display_name: Profilnomo
       domain: Domeno
-      edit: Modifikez
+      edit: Redaktar
       email: E-mail
       email_status: Retpostostando
       enable: Defrostigez
-      enabled: Aktivigita
+      enable_sign_in_token_auth: Ebligar retpostofichyurizo
+      enabled: Ebligita
       enabled_msg: Sucesoze desfrostigas konto di %{username}
       followers: Uzanti
       follows: Uzati
-      header: Kapimajo
+      header: Fundimajo
       inbox_url: URL di mesajbuxo
-      invite_request_text: Juntomotivo
+      invite_request_text: Adeskmotivi
       invited_by: Invitesis da
       ip: IP
-      joined: Juntita
+      joined: Adeskis
       location:
         all: Omna
         local: Lokala
         remote: Nelokala
         title: Loko
       login_status: Enirstando
-      media_attachments: Mediiatachaji
+      media_attachments: Audvidajaddonaji
       memorialize: Memorializez
       memorialized: Memorializita
       memorialized_msg: Sucesoze chanjesis %{username} a memorialkonto
@@ -110,15 +117,15 @@ io:
       public: Publika
       push_subscription_expires: Abono PuSH expiras
       redownload: Rifreshigez profilo
-      redownloaded_msg: Sucesoze rifreshis profilo di %{username} de origino
+      redownloaded_msg: Sucese rifreshigis profilo di %{username} de origino
       reject: Refuzez
       rejected_msg: Sucesoze refuzis registroapliko di %{username}
       remote_suspension_irreversible: La datumi di ca konto esas nerenversebla efacita.
       remote_suspension_reversible_hint_html: Ca konto restriktesis che lua servilo, e la datumi tota efacesos ye %{date}. Ante ta tempo, la fora servilo povos restaurar ca konto sen irga mala efecti. Se vu volas efacar omna datumi dil konto quik, vu povas facar lo sube.
       remove_avatar: Efacez profilimajo
-      remove_header: Efacez kapimajo
+      remove_header: Forigar fundimajo
       removed_avatar_msg: Sucesoze efacis profilimajo di %{username}
-      removed_header_msg: Sucesoze efacis kapimajo di %{username}
+      removed_header_msg: Sucese forigis fundimajo di %{username}
       resend_confirmation:
         already_confirmed: Ca uzanto ja konfirmesis
         send: Risendez konfirmligilo
@@ -128,6 +135,7 @@ io:
       resubscribe: Riabonez
       role: Rolo
       search: Trovez
+      search_same_email_domain: Altra uzanti kun la sama retpostodomeno
       search_same_ip: Altra uzanti kun sama IP
       security: Sekureso
       security_measures:
@@ -147,7 +155,7 @@ io:
       suspend: Restriktez
       suspended: Restriktita
       suspension_irreversible: Ca informi di ca konto neinversigeble efacesis. Vu povas derestriktar konto por uzebligar lu ma ol ne riganos irga dati quon ol antee havis.
-      suspension_reversible_hint_html: Ca konto restriktesis, e ca informi tota efacesos en %{date}. Ante ta tempo, konto povas riganesar sen irga mala efekti. Se vu volas efacar omna informi di konto quik, vu povas agar lu sube.
+      suspension_reversible_hint_html: Ca konto blokusesis, e ca informi tota forigesos ye %{date}. Se vu volas forigar omna informi di konto quik, vu povas agar lu dessupre.
       title: Konti
       unblock_email: Deobstruktez retpostoadreso
       unblocked_email_msg: Sucesoze deobstruktis retpostoadreso di %{username}
@@ -168,33 +176,45 @@ io:
         approve_appeal: Aprobez apelo
         approve_user: Aprobez uzanto
         assigned_to_self_report: Taskigez raporto
+        change_email_user: Chanjar retpostoadreso por Uzanto
         change_role_user: Chanjez rolo di uzanto
         confirm_user: Konfirmez uzanto
         create_account_warning: Kreez averto
         create_announcement: Kreez anunco
+        create_canonical_email_block: Krear retpostoblokuso
         create_custom_emoji: Kreez kustumizita emocimajo
         create_domain_allow: Kreez domenpermiso
         create_domain_block: Kreez domenobstrukto
+        create_email_domain_block: Krear retpostodomenblokuso
         create_ip_block: Kreez IP-regulo
+        create_relay: Krear relayo
         create_unavailable_domain: Kreez nedisponebla domeno
         create_user_role: Kreez rolo
         demote_user: Despromocez uzanto
         destroy_announcement: Efacez anunco
+        destroy_canonical_email_block: Forigar retpostoblokuso
         destroy_custom_emoji: Efacez kustumizita emocimajo
         destroy_domain_allow: Efacez domenpermiso
         destroy_domain_block: Efacez domenobstrukto
+        destroy_email_domain_block: Forigar retpostodomenblokuso
         destroy_instance: Efacez domeno
         destroy_ip_block: Efacez IP-regulo
+        destroy_relay: Forigar relayo
         destroy_status: Efacez posto
         destroy_unavailable_domain: Efacez nedisponebla domeno
         destroy_user_role: Destruktez rolo
-        disable_2fa_user: Desaktivigez 2FA
-        disable_custom_emoji: Desaktivigez kustumizita emocimajo
-        disable_user: Desaktivigez uzanto
-        enable_custom_emoji: Aktivigez kustumizita emocimajo
+        disable_2fa_user: Desebligar 2FA
+        disable_custom_emoji: Desebligar personesigita emocimajo
+        disable_relay: Desebligar relayo
+        disable_sign_in_token_auth_user: Desebligar retpostofichyurizo por uzanto
+        disable_user: Desaktivigar uzanto
+        enable_custom_emoji: Ebligar personesigita emocimajo
+        enable_relay: Ebligar relayo
+        enable_sign_in_token_auth_user: Ebligar retpostofichyurizo por uzanto
         enable_user: Aktivigez uzanto
         memorialize_account: Memorializez konto
         promote_user: Promocez uzanto
+        publish_terms_of_service: Publikigar servtermini
         reject_appeal: Refuzez apelo
         reject_user: Refuzez uzanto
         remove_avatar_user: Efacez profilimajo
@@ -214,56 +234,72 @@ io:
         update_custom_emoji: Novigez kustumizita emocimajo
         update_domain_block: Novigez domenobstrukto
         update_ip_block: Kreez IP-regulo
+        update_report: Tildatigar raporto
         update_status: Novigez posto
         update_user_role: Novigez rolo
       actions:
         approve_appeal_html: "%{name} aprobis jerdecidapelo de %{target}"
         approve_user_html: "%{name} aprobis registro de %{target}"
         assigned_to_self_report_html: "%{name} taskigis raporto %{target} a su"
+        change_email_user_html: "%{name} chanjis la retpostoadreso di uzanto %{target}"
         change_role_user_html: "%{name} chanjis rolo di %{target}"
+        confirm_user_html: "%{name} konfirmis retpostoadreso di uzanto %{target}"
         create_account_warning_html: "%{name} sendis averto a %{target}"
         create_announcement_html: "%{name} kreis nova anunco %{target}"
-        create_custom_emoji_html: "%{name} adchargis nova emocimajo %{target}"
+        create_canonical_email_block_html: "%{name} blokusis retposto kun la greto %{target}"
+        create_custom_emoji_html: "%{name} adkargis nova emocimajo %{target}"
         create_domain_allow_html: "%{name} permisis federato kun domeno %{target}"
         create_domain_block_html: "%{name} obstruktis domeno %{target}"
+        create_email_domain_block_html: "%{name} blokusis retpostodomeno %{target}"
         create_ip_block_html: "%{name} kreis regulo por IP %{target}"
+        create_relay_html: "%{name} kreis relayo %{target}"
         create_unavailable_domain_html: "%{name} cesis sendo a domeno %{target}"
         create_user_role_html: "%{name} kreis rolo di %{target}"
         demote_user_html: "%{name} despromocis uzanto %{target}"
         destroy_announcement_html: "%{name} efacis anunco %{target}"
+        destroy_canonical_email_block_html: "%{name} desblokusis retposto kun greto %{target}"
         destroy_custom_emoji_html: "%{name} efacis emocimajo %{target}"
         destroy_domain_allow_html: "%{name} despermisis federato kun domeno %{target}"
         destroy_domain_block_html: "%{name} deobstruktis domeno %{target}"
+        destroy_email_domain_block_html: "%{name} desblokusis retpostodomeno %{target}"
         destroy_instance_html: "%{name} efacis domeno %{target}"
         destroy_ip_block_html: "%{name} efacis regulo por IP %{target}"
+        destroy_relay_html: "%{name} forigis la relayo %{target}"
         destroy_status_html: "%{name} efacis posto da %{target}"
         destroy_unavailable_domain_html: "%{name} durigis sendo a domeno %{target}"
         destroy_user_role_html: "%{name} efacis rolo di %{target}"
-        disable_2fa_user_html: "%{name} desaktivigis 2-faktorbezono por uzanto %{target}"
-        disable_custom_emoji_html: "%{name} desaktivigis emocimajo %{target}"
-        disable_user_html: "%{name} desaktivigis eniro por uzanto %{target}"
-        enable_custom_emoji_html: "%{name} aktivigis emocimajo %{target}"
-        enable_user_html: "%{name} aktivigis eniro por uzanto %{target}"
+        disable_2fa_user_html: "%{name} desebligis dufaktora bezono por uzanto %{target}"
+        disable_custom_emoji_html: "%{name} desebligis emocimajo %{target}"
+        disable_relay_html: "%{name} desebligis la relayo %{target}"
+        disable_sign_in_token_auth_user_html: "%{name} desebligis retpostofichyurizo por %{target}"
+        disable_user_html: "%{name} desebligis eniro por uzanto %{target}"
+        enable_custom_emoji_html: "%{name} ebligis emocimajo %{target}"
+        enable_relay_html: "%{name} ebligis la relayo %{target}"
+        enable_sign_in_token_auth_user_html: "%{name} ebligis retpostofishyurizo por %{target}"
+        enable_user_html: "%{name} ebligis eniro por uzanto %{target}"
         memorialize_account_html: "%{name} kauzigis konto di %{target} divenar memorialpagino"
         promote_user_html: "%{name} promocis uzanto %{target}"
+        publish_terms_of_service_html: "%{name} publikigis tildatigi ad la servtermini"
         reject_appeal_html: "%{name} refuzis jerdecidapelo de %{target}"
         reject_user_html: "%{name} refuzis registro de %{target}"
         remove_avatar_user_html: "%{name} efacis profilimajo de %{target}"
         reopen_report_html: "%{name} riapertis raporto %{target}"
+        resend_user_html: "%{name} risendis konfirmretposto por %{target}"
         reset_password_user_html: "%{name} richanjis pasvorto de uzanto %{target}"
         resolve_report_html: "%{name} rezolvis raporto %{target}"
-        sensitive_account_html: "%{name} markizis medii di %{target} quale sentoza"
+        sensitive_account_html: "%{name} markis audvidaji di %{target} quale trublema"
         silence_account_html: "%{name} limitizis konto di %{target}"
         suspend_account_html: "%{name} restriktis konto di %{target}"
         unassigned_report_html: "%{name} detaskigis raporto %{target}"
         unblock_email_account_html: "%{name} deobstruktis retpostoadreso di %{target}"
-        unsensitive_account_html: "%{name} demarkizis medii di %{target} quale sentoza"
+        unsensitive_account_html: "%{name} desmarkizis audvidaji di %{target} quale trublema"
         unsilence_account_html: "%{name} efacis limito di konto di %{target}"
         unsuspend_account_html: "%{name} derestriktis konto di %{target}"
         update_announcement_html: "%{name} novigis anunco %{target}"
         update_custom_emoji_html: "%{name} novigis emocimajo %{target}"
         update_domain_block_html: "%{name} novigis domenobstrukto por %{target}"
         update_ip_block_html: "%{name} kreis regulo por IP %{target}"
+        update_report_html: "%{name} tildatigis raporto %{target}"
         update_status_html: "%{name} novigis posto da %{target}"
         update_user_role_html: "%{name} chanjis rolo di %{target}"
       deleted_account: konto efacita
@@ -271,10 +307,11 @@ io:
       filter_by_action: Filtrez segun ago
       filter_by_user: Filtrez segun uzanto
       title: Kontrollogo
+      unavailable_instance: "(domennomo nedisponebla)"
     announcements:
       destroyed_msg: Anunco sucesoze efacesas!
       edit:
-        title: Modifikez anunco
+        title: Redaktar anunco
       empty: Nula anunci.
       live: Samtempe
       new:
@@ -290,7 +327,7 @@ io:
       updated_msg: Anunco sucesoza novigesas!
     critical_update_pending: Urjanta aktualigo vartesas
     custom_emojis:
-      assign_category: Insertez kategorio
+      assign_category: Juntar kategorio
       by_domain: Domeno
       copied_msg: Sucesoze kreis lokala kopiuro di emocimajo
       copy: Kopiez
@@ -299,18 +336,18 @@ io:
       created_msg: Emocimajo sucesoze kreesas!
       delete: Efacez
       destroyed_msg: Kustumizita emocimajo sucesoza destruktesas!
-      disable: Desaktivigez
-      disabled: Desaktivigita
-      disabled_msg: Sucesoze desaktivigis ta emocimajo
+      disable: Desebligar
+      disabled: Desebligita
+      disabled_msg: Sucese desebligis ta emocimajo
       emoji: Emocimajo
-      enable: Aktivigez
-      enabled: Aktivigita
-      enabled_msg: Sucesoze aktivigis ta emocimajo
+      enable: Ebligar
+      enabled: Ebligita
+      enabled_msg: Sucese ebligis ta emocimajo
       image_hint: Maximo grandeso di PNG o GIF esas %{size}
       list: Listo
       listed: Listita
       new:
-        title: Insertez nova kustumizita emocimajo
+        title: Adjuntar nova personesigita emocimajo
       no_emoji_selected: Nula emocimaji chanjesis pro ke nulo selektesis
       not_permitted: Vu ne permisesis agar co
       overwrite: Remplasez
@@ -322,11 +359,11 @@ io:
       unlisted: Delistigita
       update_failed_msg: Ne povas novigar ta emocimajo
       updated_msg: Emocimajo sucesoze novigesis!
-      upload: Adchargez
+      upload: Adkargar
     dashboard:
       active_users: aktiva uzanti
       interactions: interagi
-      media_storage: Mediireteneso
+      media_storage: Audvidajkonservo
       new_users: nova uzanti
       opened_reports: raporti apertesis
       pending_appeals_html:
@@ -374,7 +411,7 @@ io:
       created_msg: Domenobstrukto nun procedesas
       destroyed_msg: Domenobstrukto desagesis
       domain: Domeno
-      edit: Modifikez domenobstrukto
+      edit: Redaktar domenblokuso
       existing_domain_block: Vu ja exekutis plu rigoroza limiti a %{name}.
       existing_domain_block_html: Vu ja povis plu rigoroza limiti a %{name}, vu bezonas <a href="%{unblock_url}">deobstruktar</a> unesme.
       export: Exportez
@@ -391,33 +428,41 @@ io:
       no_domain_block_selected: Nula domenobstrukti ne chanjesis por ke nulo selektesis
       not_permitted: Vu ne permisesis agar co
       obfuscate: Nedicernebligez domennomo
-      obfuscate_hint: Partala nedicernebligez domennomo en listo se reklamo di listo di domenlimito aktivigesas
+      obfuscate_hint: Parte celegez domennomo en la listo se reklamago di listo di domenlimito es ebligita
       private_comment: Privata komento
       private_comment_hint: Komento pri ca domenlimito esas por interna uzo da jereri.
       public_comment: Publika komento
-      public_comment_hint: Komentez pri ca domenlimito por la publiko, se reklamar listo di domenlimito aktivigesis.
-      reject_media: Refusez mediifaili
-      reject_media_hint: Efacas lokale retenita mediifaili e refuzas deschargar irgo en futuro. Nerelatata por restrikti
+      public_comment_hint: Komentez pri ca domenlimito por la publiko, se reklamago di listo di domenlimito es ebligita.
+      reject_media: Desaceptar audvidajdosieri
+      reject_media_hint: Forigas lokale konservita audvidajdosieri e desvolas deskargar irgo en la estonteso
       reject_reports: Refuzez raporti
       reject_reports_hint: Ignorez omna raporti quo venas de ca domeno. Nerelatata por restrikti
       undo: Undo
       view: Videz domenobstrukto
     email_domain_blocks:
-      add_new: Insertez novo
+      add_new: Adjuntar novo
+      allow_registrations_with_approval: Permisar registri kun aprobo
       attempts_over_week:
         one: "%{count} probo de pos 1 week"
         other: "%{count} registroprobi de pos 1 week"
+      created_msg: Sucese blokusis retpostodomeno
       delete: Efacez
       dns:
         types:
           mx: Rekordo MX
       domain: Domeno
       new:
-        create: Insertez domeno
+        create: Adjuntar domeno
         resolve: Rezolvez domeno
+        title: Blokusar nova retpostodomeno
+      no_email_domain_block_selected: Nula retpostodomenblokusi chanjesis pro ke nulo selektesis
       not_permitted: Ne permisata
+      resolved_dns_records_hint_html: La domennomo indikas ad la desantea MX-domeni kua chefe aceptas retposto. <strong>Zorgemez e ne blokusez ega retpostoprovizanti.</strong>
       resolved_through_html: Rezolvesis tra %{domain}
+      title: Blokusita retpostodomeni
     export_domain_allows:
+      new:
+        title: Importacar domenpermisi
       no_file: Nula dosiero selektesas
     export_domain_blocks:
       import:
@@ -439,6 +484,9 @@ io:
       title: Sequez rekomendi
       unsuppress: Riganez sequorekomendo
     instances:
+      audit_log:
+        title: Lastatempa kontrolnoti
+        view_all: Vidar plena kontrolnoti
       availability:
         description_html:
           one: Se sendar a domeno falias <strong>%{count} dio</strong> sen sucesar, plusa sendoprobi ne agesos se sendo <em>de</em> la domeno ne ganesas.
@@ -460,7 +508,7 @@ io:
         description_html: Vu povas fixar kontenajguidili quo aplikesos a omna konti de ca domeno e irga oli subdomeni.
         limited_federation_mode_description_html: Vu povas selektar ka vu permisos federar kun ca domeno.
         policies:
-          reject_media: Refusez medii
+          reject_media: Desaceptar audvidaji
           reject_reports: Refusez raporti
           silence: Limito
           suspend: Restriktez
@@ -473,7 +521,7 @@ io:
         instance_followers_measure: nia sequanti ibe
         instance_follows_measure: olia sequanti hike
         instance_languages_dimension: Maxim uzata lingui
-        instance_media_attachments_measure: retenata mediiatachaji
+        instance_media_attachments_measure: konservita audvidajaddonaji
         instance_reports_measure: raporti pri oli
         instance_statuses_measure: retenata posti
       delivery:
@@ -504,11 +552,11 @@ io:
       total_followed_by_them: Sequesis da oli
       total_followed_by_us: Sequesis da ni
       total_reported: Raporti pri oli
-      total_storage: Mediiatachaji
+      total_storage: Audvidajaddonaji
       totals_time_period_hint_html: Sumi quo montresas sube inkluzas informi de pos la komenco.
       unknown_instance: Prezente ne esas registrago pri ta domeno che ca servilo.
     invites:
-      deactivate_all: Deaktivigez omno
+      deactivate_all: Desebligar omno
       filter:
         all: Omna
         available: Disponebla
@@ -517,7 +565,7 @@ io:
       title: Inviti
     ip_blocks:
       add_new: Kreez regulo
-      created_msg: Sucesoze insertis nova regulo di IP
+      created_msg: Sucese adjuntis nova IP-regulo
       delete: Efacez
       expires_in:
         '1209600': 2 semani
@@ -533,19 +581,19 @@ io:
     relationships:
       title: "%{acct} relatesi"
     relays:
-      add_new: Insertez nova relayo
+      add_new: Adjuntar nova relayo
       delete: Efacez
-      description_html: "<strong>Federatrelayo</strong> esas mediatajservilo quo interchanjas multa publika posti inter servili quo abonas e publikigas ibe. <strong>Ol povas helpar mikra e mezgranda servili deskovrar kontenajo de fediverso</strong>, quale ne bezonas lokala uzanti manuale sequar altra personi en nelokala servili."
-      disable: Desaktivigez
-      disabled: Desaktivigita
-      enable: Aktivigez
-      enable_hint: Pos aktivigesar, vua servilo abonos omna publika posti de ca relayo, e komencos sendar publika posti di ca servilo a ol.
-      enabled: Aktivigita
+      description_html: "<strong>Fratarrelayo</strong> es intermedia servilo qua interchanjas multa publika afishi inter servili qua abonas e publikigas ibe."
+      disable: Desebligar
+      disabled: Desebligita
+      enable: Ebligar
+      enable_hint: Pos ebligita, vua servilo abonos omna publika afishi de ca relayo, e komencos sendar publika afishi di ca servilo ad ol.
+      enabled: Ebligita
       inbox_url: URL di relayo
       pending: Vartas aprobo di relayo
-      save_and_enable: Sparez e aktivigez
+      save_and_enable: Konservar e ebligar
       setup: Facez relayokonekto
-      signatures_not_enabled: Relayi ne korekta funcionos dum ke sekurmodo o limigita federatmodo aktivigesis
+      signatures_not_enabled: Relayi eble ne korekta funcionos dum ke sekurmodo o limigita fratarmodo es ebligita
       status: Stando
       title: Relayi
     report_notes:
@@ -560,13 +608,18 @@ io:
       action_taken_by: Agesis da
       actions:
         delete_description_html: Raportizita posti efacesos e streko rekordigesos por helpar vu intensigar en nexta malagi da la sama konto.
-        mark_as_sensitive_description_html: Medii en raportizita posti markizesos quale sentoza e streko rekordigesos por helpar vu intensigar en nexta malagi da la sama konto.
-        other_description_html: Videz plu multa opcioni por dominacar konduto di konto e kustumizar komuniko a raportizita konto.
+        mark_as_sensitive_description_html: Audvidaji en raportita afishi markesos quale trublema e streko rekordesos por helpar vu intensigar en posa malagi da la sama konto.
+        other_description_html: Videz plu multa preferaji por regularar konduto di konto e personesigar komuniko ad raportita konto.
         resolve_description_html: Nulo agesos kontre raportizita konto, streko ne rekordizesos e raporto klozesos.
         silence_description_html: Konto esos videbla nur por personi qui ja sequas lo o manuale serchas lo, severe limitizante lua atingo. On sempre povas desfacar co. Klozas omna raporti kontra ca konto.
         suspend_description_html: Ca konto e omna kontenaji esos neacesebla e efacota, e interagar kun ol esos neposibla. Desfacebla dum 30 dii. Klozas omna raporti kontra ca konto.
+      actions_description_html: Decidez quala ago por traktar ca raporto.
       actions_description_remote_html: Selektez quo vu agos por solvar ca raporto. Co nur efektigos quale <strong>vua</strong> servilo komunikas kun ca fora konto e traktas lua kontenaji.
-      add_to_report: Insertez pluse a raporto
+      actions_no_posts: Ca raporto ne havas irga relatita afishi por forigar
+      add_to_report: Adjuntar plu ad raporto
+      already_suspended_badges:
+        local: Ja blokusita sur ca servilo
+        remote: Ja blokusita sur lua servilo
       are_you_sure: Ka vu esas certa?
       assign_to_self: Taskigez me
       assigned: Taskigita jerero
@@ -589,7 +642,7 @@ io:
       mark_as_unresolved: Markizez quale nerezolvita
       no_one_assigned: Nulu
       notes:
-        create: Insertez noto
+        create: Adjuntar noto
         create_and_resolve: Rezolvez per noto
         create_and_unresolve: Riapertez per noto
         delete: Efacez
@@ -603,6 +656,7 @@ io:
       report: 'Raporto #%{id}'
       reported_account: Raportizita konto
       reported_by: Raportizesis da
+      reported_with_application: Raportesis per apliko
       resolved: Rezolvesis
       resolved_msg: Raporto sucesoze rezolvesis!
       skip_to_actions: Saltez a agi
@@ -625,6 +679,7 @@ io:
         delete_data_html: Efacor la profilo e kontenaji di <strong>@%{acct}</strong> ye 30 dii de nun ecepte ke lu esus desrestrikita ante ta tempe
         preview_preamble_html: "<strong>@%{acct}</strong> recevos averto kun la sequanta kontenajo:"
         record_strike_html: Registrar punto kontra <strong>@%{acct}</strong> por helpar vu traktar futura reguloviolaci di ca konto
+        send_email_html: Sendar ad <strong>@%{acct}</strong> avertretposton
         warning_placeholder: Neobligata plusa expliko por la jero-ago.
       target_origin: Fonto di raportizita konto
       title: Raporti
@@ -634,18 +689,19 @@ io:
       updated_at: Novigesis
       view_profile: Videz profilo
     roles:
-      add_new: Insertez rolo
+      add_new: Adjuntar rolo
       assigned_users:
         one: "%{count} uzanto"
         other: "%{count} uzanti"
       categories:
         administration: Administro
+        devops: Developisto
         invites: Inviti
         moderation: Jero
         special: Specala
       delete: Efacez
       description_html: Per <strong>uzantoroli</strong>, vu povas kustumizar funciono e siti di Mastodon quon vua uzanti povas uzar.
-      edit: Modifikez rolo di '%{name}'
+      edit: Redaktar rolo di '%{name}'
       everyone: Originala permisi
       everyone_full_description_html: Co esas <strong>bazrolo</strong> quo efektigas <strong>omna uzanti</strong>, mem personi sen rolo. Omna altra roli ganas sama permisi de ol.
       permissions_count:
@@ -663,23 +719,25 @@ io:
         manage_appeals: Jerez apeli
         manage_appeals_description: Permisez uzanti kontrolar apeli kontra jero
         manage_blocks: Jerez obstrukti
+        manage_blocks_description: Permisar uzanti blokusar retpostoservili e IP-adresi
         manage_custom_emojis: Jerez kustumizita emocimaji
         manage_custom_emojis_description: Permisez uzanti jerar kustumizita emocimaji en la servilo
         manage_federation: Jerez federo
         manage_federation_description: Permisez uzanti obstruktar o permisez federo kun altra domeni, e dominacar sendebleso
         manage_invites: Jerez inviti
-        manage_invites_description: Permisez uzanti vidar e desaktivigar invitligili
+        manage_invites_description: Permisez uzanti vidar e desebligar invitligili
         manage_reports: Jerez raporti
         manage_reports_description: Permisez uzanti kontrolar raporti e jerez kontra oli
         manage_roles: Jerez roli
         manage_roles_description: Permisez uzanti jerar e ajustar plu basa roli di olia
         manage_rules: Jerez reguli
         manage_rules_description: Permisez uzanti chanjar servilreguli
-        manage_settings: Jerez opcioni
-        manage_settings_description: Permisez uzanti chanjar sitopcioni
+        manage_settings: Administrar preferaji
+        manage_settings_description: Permisez uzanti chanjar reteypreferaji
         manage_taxonomies: Jerez nomkategorii
-        manage_taxonomies_description: Permisez uzanti kontrolar tendencoza kontenajo e novigar hashtagopcioni
+        manage_taxonomies_description: Permisez uzanti kontrolar populareska enhavajo e tildatigar gretvortpreferaji
         manage_user_access: Jerez uzantoeniro
+        manage_user_access_description: Permisar uzanti desebligar dufaktoryurizo di altra uzanti, chanjar retpostoadresi e richanjar pasvorto
         manage_users: Jerez uzanti
         manage_users_description: Permisez uzanti vidar detali di altra uzanti e jerar kontra oli
         manage_webhooks: Jerez interrethoki
@@ -687,14 +745,15 @@ io:
         view_audit_log: Videz kontrollogo
         view_audit_log_description: Permisez uzanti vidar historio di administrala agi en la servilo
         view_dashboard: Videz chefpanelo
-        view_dashboard_description: Permisez uzanti uzar chefpanelo e diversa opcioni
+        view_dashboard_description: Permisez uzanti uzar chefpanelo e diversa mezuri
+        view_devops: Developisto
         view_devops_description: Permisez uzanti uzar chefpaneli Sidekiq e pgHero
       title: Roli
     rules:
-      add_new: Insertez regulo
+      add_new: Adjuntar regulo
       delete: Efacez
       description_html: Quankam maxim multa personi asertar ke lu ja lektis e konsentis serveskondicioni, frequente personi ne komplete lektas til problemo eventas. <strong>Faciligez on vidar vua reguli di servilo kurte per pozar puntostrukturlisto.</strong> Lasez singla reguli esar kurta e simpla, ma ne fendez a multa separata kozi anke.
-      edit: Modifikez regulo
+      edit: Redaktar regulo
       empty: Nula servilreguli fixesis til nun.
       title: Servilreguli
     settings:
@@ -710,9 +769,11 @@ io:
         preamble: Fabrikmarko di ca servilo diferentigas lu de altra servili en la reto. Ca informi forsan montresas che diversa loki. Do, ca informi debas esar klara.
         title: Fabrikmarkeso
       captcha_enabled:
+        desc_html: Co dependas externa skripti de hCaptcha kua eble es sekureso e privatesdesquieto. Do, uzez alternativa metodi tala aprobala o invitala registro.
         title: Postular ke nova uzanti solvos CAPTCHA por konfirmar lia konti
       content_retention:
-        preamble: Dominacez quale uzantigita kontenajo retenesar en Mastodon.
+        danger_zone: Danjerzono
+        preamble: Selektar quale uzantigita kontenajo retenesar en Mastodon.
         title: Kontenajreteneso
       default_noindex:
         desc_html: Efektigas omna uzanti qui ne personale chanjis ca opciono
@@ -720,17 +781,18 @@ io:
       discovery:
         follow_recommendations: Sequez rekomendaji
         preamble: Montrar interesanta kontenajo esas importanta ye voligar nova uzanti quo forsan ne savas irgu. Dominacez quale ca deskovrotraiti funcionar en ca servilo.
-        profile_directory: Profilcheflisto
+        profile_directory: Profiluyo
         public_timelines: Publika tempolinei
         publish_discovered_servers: Publikar deskovrita servili
         publish_statistics: Publikar statistiki
         title: Deskovro
-        trends: Tendenci
+        trends: Populari
       domain_blocks:
         all: A omnu
         disabled: A nulu
         users: A enirinta lokala uzanti
       registrations:
+        moderation_recommandation: Certez ke vu havas sata e reaktiva jergrupo ante vu desklozar registri ad omnu!
         preamble: Dominacez qua povas krear konto en ca servilo.
         title: Registragi
       registrations_mode:
@@ -738,6 +800,7 @@ io:
           approved: Aprobo bezonesas por registro
           none: Nulu povas registrar
           open: Irgu povas registrar
+        warning_hint: Ni rekomendas uzar "Aprobo bezonita por registro" se vu ne kunfidas ke vua jergrupo povas traktar spamo e mala registri bontempe.
       security:
         authorized_fetch: Postular autentikigo de federata servili
         authorized_fetch_hint: Postular autentikigo de federata servili kapabligar plu strikta enforcigo dil blokusi di uzanti e dil servilo. Tamen, co enduktos exekutado-lentigo, diminutos la atingebleso di via respondi, e forsan enduktos koncilieblesoproblemi kun kelka softwari federata. Pluse, co ne preventos aganti dedikita de acesar vua publika posti e konti.
@@ -745,10 +808,11 @@ io:
         federation_authentication: Enforcigo di federado-autentikigo
       title: Servilopcioni
     site_uploads:
-      delete: Efacez adchargita failo
-      destroyed_msg: Sitadchargito sucesoze efacesis!
+      delete: Forigar adkargita dosiero
+      destroyed_msg: Reteyadkargo sucese forigesis!
     software_updates:
       critical_update: Urjanta — voluntez aktualigar rapide
+      description: On rekomendesis lasar Mastodon-instalo esar tildatigita por bone ganar maxim nova repari e traiti. Do, Mastodon kontrolas tildatigi ye singla 30 minuti, e savigos vu segun vua retpostosavigpreferi.
       documentation_link: Lernez pluse
       release_notes: Emiso-noti
       title: Aktualigi disponebla
@@ -764,24 +828,31 @@ io:
       back_to_account: Retrovenez a kontopagino
       back_to_report: Retrovenez a raportpagino
       batch:
+        add_to_report: 'Adjuntar ad raporto #%{id}'
         remove_from_report: Efacez de raporto
         report: Raportizez
+      contents: Enhavaji
       deleted: Efacesis
       favourites: Favoriziti
       history: Historio di versioni
       in_reply_to: Respondante a
       language: Linguo
       media:
-        title: Medii
+        title: Audvidaji
       metadata: Metadatumi
+      no_history: Ca afisho ne redaktesis
       no_status_selected: Nula posti chanjesis pro ke nulo selektesis
       open: Apertez posto
       original_status: Originala posto
       reblogs: Dissemi
+      replied_to_html: Respondis ad %{acct_link}
       status_changed: Posto chanjita
+      status_title: Afisho da %{name}
+      title: Kontoafishi - @%{name}
       trending: Populara
+      view_publicly: Vidar publike
       visibility: Videbleso
-      with_media: Kun medii
+      with_media: Kun audvidaji
     strikes:
       actions:
         delete_statuses: "%{name} efacis posti di %{target}"
@@ -797,12 +868,20 @@ io:
     system_checks:
       database_schema_check:
         message_html: Existas vartanta datamaturmigri. Startez por certigar ke la softwaro kondutar quale expektita
+      elasticsearch_health_red:
+        message_html: Elastiksercharo es nekapabla, serchtraiti es nedisponebla
+      elasticsearch_health_yellow:
+        message_html: Elastiksercharo es nekapabla, vu darfas volar inquestar la motivo
       elasticsearch_preset:
         action: Videz la dokumentajo
+        message_html: Vua elastiksercharo havas plu kam un nodo, ma Mastodon ne povas uzar ili.
       elasticsearch_preset_single_node:
         action: Videz la dokumentajo
+        message_html: Vua elastiksercharo havas nur un nodo, <code>ES_PRESET</code> devas fixesesar ad <code>single_node_cluster</code>.
+      elasticsearch_reset_chewy:
+        message_html: Vua elastikserchsistemindexo esas destildatigita pro preferajchanjo.
       elasticsearch_running_check:
-        message_html: Ne povas konektas a Elasticsearch. Kontrolez ke ol functionas o desaktivigez textokompleta trovo
+        message_html: Ne povas konektas ad Elasticsearch.
       elasticsearch_version_check:
         message_html: 'Nekonciliebla versiono di Elasticsearch: %{value}'
         version_comparison: Elasticsearch %{running_version} funcionas ma %{required_version} bezonesas
@@ -810,7 +889,10 @@ io:
         action: Jerez servilreguli
         message_html: Vu ne fixis irga servilreguli.
       sidekiq_process_check:
-        message_html: Sidekiq procedo ne funcionas ye %{value} fask(o). Kontrolez vua opciono di Sidekiq
+        message_html: Sidekiq procedo ne funcionas ye %{value} fask(o)
+      software_version_check:
+        action: Vidar disponebla tildatigi
+        message_html: Mastodon-tildatigo es disponebla.
       software_version_critical_check:
         action: Videz la aktualigi disponebla
         message_html: Urjanta Mastodon-aktualigo es disponebla, voluntez aktualigar la servilo tam rapide kam es posibla.
@@ -824,16 +906,66 @@ io:
         action: Konsultez hike por plu multa informo
         message_html: "<strong>Vua dosierokonservo es misfigurizita. La privateso di vua uzanti es domajebla.</strong>"
     tags:
+      moderation:
+        not_trendable: Ne popularebla
+        not_usable: Ne uzebla
+        pending_review: Vartas kontrolo
+        review_requested: Kontrolo demandesis
+        reviewed: Kontrolesis
+        title: Stando
+        trendable: Popularebla
+        unreviewed: Nekontrolesis
+        usable: Neuzebla
+      name: Nomo
+      newest: Maxim nova
+      oldest: Maxim desnova
+      open: Vidar publike
+      reset: Richanjar
       review: Kontrolez stando
-      updated_msg: Hashtagopcioni novigesis sucesoze
+      search: Serchar
+      title: Gretvorti
+      updated_msg: Gretvortpreferaji tildatigesis sucese
+    terms_of_service:
+      back: Retroirar ad servtermini
+      changelog: Quo chanjesis
+      create: Uzar vua sua ajo
+      current: Nuntempa
+      draft: Skribureto
+      generate: Uzar shablono
+      generates:
+        action: Igar
+        chance_to_review_html: "<strong>Igita servtermini ne publikigos automate.</strong> Plenigez la necesa detali por daurar."
+      history: Historio
+      live: Nuna
+      no_history: Ne havas rekordita chanji di servtermini ankore.
+      no_terms_of_service_html: Vu nune ne havas irga fixita servtermini.
+      notified_on_html: Uzanti savigesis ye %{date}
+      notify_users: Savigar uzanti
+      preview:
+        explanation_html: La retposto sendesos ad <strong>%{display_count}uzanti</strong> qua registris ante %{date}.
+        send_preview: Sendar prevido ad %{email}
+        send_to_all:
+          one: Sendar %{display_count} retposto
+          other: Sendar %{display_count} retposti
+        title: Previdar servterminsavigo
+      publish: Publikigar
+      published_on_html: Publikigita ye %{date}
+      save_draft: Konservar skiso
+      title: Servtermini
     title: Administro
     trends:
       allow: Permisez
       approved: Aprobesis
+      confirm_allow: Ka vu es certa ke vu volas permisar selektita etiketi?
+      confirm_disallow: Ka vu es certa ke vu volas despermisar selektita etiketi?
       disallow: Despermisez
       links:
         allow: Permisez ligilo
         allow_provider: Permisez publikiganto
+        confirm_allow: Ka vu es certa ke vu volas permisar selektita ligili?
+        confirm_allow_provider: Ka vu es certa ke vu volas permisar selektita provizanti?
+        confirm_disallow: Ka vu es certa ke vu volas despermisar selektita ligili?
+        confirm_disallow_provider: Ka vu es certa ke vu volas despermisar selektita provizanti?
         description_html: Co esas ligili quo nun multe partigesas da konti kun posti quon vua servilo vidas. Ol povas helpar vua uzanti lernar quo eventas en mondo. Ligili ne publike montresas til vu aprobar publikiganto. Vu povas anke permisar o refuzar individuala ligili.
         disallow: Despermisez ligilo
         disallow_provider: Despermisez publikiganto
@@ -843,29 +975,33 @@ io:
         shared_by_over_week:
           one: Partigesis da 1 persono de pos antea semano
           other: Partigesis da %{count} personi de pos antea semano
-        title: Tendencoza ligili
+        title: Populara ligili
         usage_comparison: Partigesis %{today} foyi hodie, la nombro esas %{yesterday} hiere
       not_allowed_to_trend: Ne permisita quale popularo
       only_allowed: Nur permisato
       pending_review: Vartas kontrolo
       preview_card_providers:
-        allowed: Ligili de ca publikiganto povas divenar tendenco
-        description_html: Co esas domen quo havas ligili quo frequente partigesas en vua servilo. Ligili ne divenas tendenco publike se domeno di ligilo ne aprobesas. Vua aprobo (o refuzo) anke esas por subdomeni.
-        rejected: Ligili de ca publikiganto ne divenos tendenco
+        allowed: Ligili de ca publikiganto povas populareskar
+        description_html: Co esas domeni quo havas ligili qua ofte kunhavigesis che vua servilo. Vua aprobo anke esas por domeneti.
+        rejected: Ligili de ca publikiganto ne populareskos
         title: Publikiganti
       rejected: Refuzesis
       statuses:
         allow: Permisez posto
         allow_account: Permisez skribanto
-        description_html: Co esas posti quon vua servilo savas quale nun partigesas e favorizesas multe nun. Ol povas helpar vua nova e retrovenanta uzanti trovar plu multa personi por sequar. Posti ne publike montresas til vu aprobar la skribanto, e la skribanto permisas sua konto sugestesas a altra personi. Vu povas anke permisar o refuzar individuala posti.
+        confirm_allow: Ka vu es certa ke vu volas permisar selektita standi?
+        confirm_allow_account: Ka vu es certa ke vu volas permisar selektita konti?
+        confirm_disallow: Ka vu es certa ke vu volas despermisar selektita standi?
+        confirm_disallow_account: Ka vu es certa ke vu volas despermisar selektita konti?
+        description_html: Co esas afishi quan vua servilo savas e nun partigesis e stelumesis multe nun. Afishi ne publike montresas til vu aprobar la verkanto, e la verkanto permisas lua konto sugestesas ad altra personi.
         disallow: Despermisez posto
         disallow_account: Despermisez skribanto
-        no_status_selected: Nula tendencoza posti chanjesis pro ke nulo selektesis
+        no_status_selected: Nula populara afishi chanjesis pro ke nulo selektesis
         not_discoverable: Skribanto ne konsentis pri esar deskovrebla
         shared_by:
-          one: Partigesis o favorizesis 1 foye
-          other: Partigesis o favorizesis %{friendly_count} foye
-        title: Tendencoza posti
+          one: Partigesis o stelumesis unfoye
+          other: Partigesis o stelumesis %{friendly_count} foye
+        title: Populara afishi
       tags:
         current_score: Nuna punto esas %{score}
         dashboard:
@@ -878,36 +1014,38 @@ io:
         listable: Povas sugestesar
         no_tag_selected: Nula tagi chanjesis pro ke nulo selektesis
         not_listable: Ne sugestesar
-        not_trendable: Ne aparas che tendenci
+        not_trendable: Ne aparos en populari
         not_usable: Ne povas uzesar
         peaked_on_and_decaying: Maxim uzita ye %{date}, nun diminutesas
-        title: Tendencoza hashtagi
-        trendable: Povas aparar che tendenci
-        trending_rank: 'Tendencorango #%{rank}'
+        title: Populara gretvorti
+        trendable: Povas aparar en populari
+        trending_rank: 'Popularo #%{rank}'
         usable: Povas uzesar
         usage_comparison: Uzesis %{today} foyi hodie, la nombro esas %{yesterday} hiere
         used_by_over_week:
           one: Uzesis da 1 persono de pos 1 semano
           other: Uzesis da %{count} personi de pos 1 semano
-      trending: Tendenco
+      title: Rekomenditi & populari
+      trending: Populara
     warning_presets:
-      add_new: Insertez novo
+      add_new: Adjuntar novo
       delete: Efacez
-      edit_preset: Modifikez avertfixito
+      edit_preset: Redaktar avertdecido
       empty: Vu ne fixis irga avertfixito til nun.
+      title: Avertpreferaji
     webhooks:
-      add_new: Insertez finpunto
+      add_new: Adjuntar finpunto
       delete: Efacez
       description_html: "<strong>Rethoko</strong> povigas Mastodon sendar <strong>samtempoavizi</strong> pri selektita eventi a vua sua apliko, por ke vua apliko povas <strong>automate kauzigar reakti</strong>."
-      disable: Desaktivigez
-      disabled: Desaktivigita
-      edit: Modifikez finpunto
+      disable: Desebligar
+      disabled: Desebligita
+      edit: Redaktar finpunto
       empty: Vu ne havas irga ajustita finpunti ankore.
-      enable: Aktivigez
+      enable: Ebligar
       enabled: Aktiva
       enabled_events:
-        one: 1 aktivigita evento
-        other: "%{count} aktivigita eventi"
+        one: 1 ebligita evento
+        other: "%{count} ebligita eventi"
       events: Eventi
       new: Nova rethoko
       rotate_secret: Rotacigez sekreto
@@ -916,6 +1054,8 @@ io:
       title: Rethoki
       webhook: Rethok
   admin_mailer:
+    auto_close_registrations:
+      subject: Registri di %{instance} automate chanjesis ad bezonar aprobo
     new_appeal:
       actions:
         delete_statuses: efacos ola posti
@@ -944,12 +1084,12 @@ io:
     new_trends:
       body: 'Ca kozi bezonas kontrol ante ol povas montresar publike:'
       new_trending_links:
-        title: Tendencoza ligili
+        title: Populara ligili
       new_trending_statuses:
-        title: Tendencoza posti
+        title: Populara afishi
       new_trending_tags:
-        title: Tendencoza hashtagi
-      subject: Nova tendenci bezonas kontrolesar che %{instance}
+        title: Populara gretvorti
+      subject: Nova populari bezonas kontrolo sur %{instance}
   aliases:
     add_new: Kreez alternativa nomo
     created_msg: Sucesoze kreis nova alternativa nomo. Vu povas nun komencar transfero de la olda konto.
@@ -958,9 +1098,9 @@ io:
     hint_html: Se vu volas transferesar de altra konto a co, hike vu povas krear alternativnomo, quo bezonesas ante vu povas durigar transferar sequanti de la olda konto a co. Ca ago esas <strong>nedanjeroza e inversigebla</strong>. <strong>Kontomigro komencesas de la olda konto</strong>.
     remove: Deligez alternative nomo
   appearance:
-    advanced_web_interface: Avancata retintervizajo
-    advanced_web_interface_hint: 'Se vu volas uzar tota skrenlongeso, avancata retintervizajo povigas vu modifikar multa diferanta kolumni por vida multa informi en sama tempo quon vu volas: Hemo, avizi, federatata tempolineo, multa listi e hashtagi.'
-    animations_and_accessibility: Animacii e aceseso
+    advanced_web_interface: Altnivela retintervizajo
+    advanced_web_interface_hint: 'Se vu volas uzar tota skrenlongeso, altnivela retintervizajo povigas vu modifikar multa dessama kolumni por vida multa informi en sama tempo quale vu volas: Hemo, savigi, fratara tempolineo, multa listi e gretvorti.'
+    animations_and_accessibility: Animi e adirebleso
     confirmation_dialogs: Konfirmdialogi
     discovery: Deskovro
     localization:
@@ -969,7 +1109,9 @@ io:
       guide_link_text: Omnu povas kontributar.
     sensitive_content: Sentoza kontenajo
   application_mailer:
+    notification_preferences: Chanjar retpostopreferaji
     salutation: "%{name},"
+    settings: 'Chanjar retpostopreferaji: %{link}'
     unsubscribe: Desabonez
     view: 'Vidar:'
     view_profile: Videz profilo
@@ -996,17 +1138,18 @@ io:
       redirect_to_app_html: Vu devas sendesir al apliko <strong>%{app_name}</strong>. Se to ne evenis, probez %{clicking_this_link} o retroirez manuale al apliko.
       registration_complete: Vua registrago che %{domain} nun es kompleta!
       welcome_title: Bonvenez, %{name}!
+      wrong_email_hint: Se ta retpostoadreso ne es korekta, vu povas chanjar lu en la kontopreferaji.
     delete_account: Efacez konto
     delete_account_html: Se vu volas efacar vua konto, vu povas <a href="%{path}">irar hike</a>. Vu demandesos konfirmar.
     description:
-      prefix_invited_by_user: "@%{name} invitas vu juntar ca servilo di Mastodon!"
+      prefix_invited_by_user: "@%{name} invitas vu adeskar ca servilo di Mastodon!"
       prefix_sign_up: Registrez che Mastodon hodie!
-      suffix: Per konto, vu povos sequar personi, postigar novaji e interchanjar mesaji kun uzanti de irga servilo di Mastodon e pluse!
+      suffix: Per konto, vu povos sequar personi, postigar novaji e interchanjar mesaji kun uzanti de irga servilo di Mastodon e plu multo!
     didnt_get_confirmation: Ka vu ne recevis konfirmoligilo?
     dont_have_your_security_key: Ka vu ne havas sekuresklefo?
     forgot_password: Pasvorto obliviita?
     invalid_reset_password_token: Pasvorto richanjoficho esas nevalida o expirita. Demandez novo.
-    link_to_otp: Insertez 2-faktorkodexo de vua fono o rigankodexo
+    link_to_otp: Insertez dufaktora kodexo de vua telefonilo o rigankodexo
     link_to_webauth: Uzez vua sekuresklefaparato
     log_in_with: Enirez per
     login: Enirar
@@ -1015,6 +1158,7 @@ io:
     migrate_account_html: Se vu volas ridirektar ca konto a diferanto, vu povas <a href="%{path}">ajustar hike</a>.
     or_log_in_with: O eniras per
     progress:
+      confirm: Konfirmar retpostoadreso
       details: Vua detali
       review: Nia revuo
       rules: Aceptar reguli
@@ -1036,22 +1180,38 @@ io:
     security: Chanjar pasvorto
     set_new_password: Selektar nova pasvorto
     setup:
+      email_below_hint_html: Kontrolez vua spamdosieruyo, o demandez altra ligilo.
+      email_settings_hint_html: Klikez la ligilo quan ni sendis ad %{email} por komencar uzar Mastodon.
       link_not_received: Ka vu ne recevis ligilo?
+      new_confirmation_instructions_sent: Vu recevos nova retposto kun la konfirmligilo pos kelka minuti!
       title: Kontrolez vua retposti
     sign_in:
       preamble_html: Enirez per vua <strong>%{domain}</strong> detali. Se vua konto esas che altra servilo, vu ne povos enirar hike.
       title: Enirez a %{domain}
     sign_up:
       manual_review: Registragi che %{domain} es revuata da nia personaro. Por helpar ni traktar vua aplikajo, skribez kelko pri vu e pro quo vu volas konto che %{domain}.
+      preamble: Per konto en ca servilo di Mastodon, on povas sequar irga persono en ca fediverso, ne ye ube ona konto gastigesis.
       title: Ni komencigez vu en %{domain}.
     status:
       account_status: Kontostando
+      confirming: Vartas retpostokonfirmo esar kompleta.
       functional: Vua konto esas tote funcionoza.
+      pending: Vua apliko bezonas kontrolo da nia laborero. Vu ganos retposto se vua apliko aprobesis.
       redirecting_to: Vua konto esas neaktiva pro ke ol nun ridirektesos a %{acct}.
       self_destruct: Pro ke %{domain} balde klozos, vu havas nur aceso limitizata a vua konto.
       view_strikes: Videz antea streki kontre vua konto
     too_fast: Formulario sendesis tro rapide, probez itere.
     use_security_key: Uzes sekuresklefo
+    user_agreement_html: Me lektas e asentas <a href="%{terms_of_service_path}" target="_blank">servtermini</a> e <a href="%{privacy_policy_path}" target="_blank">privatesguidilo</a>
+    user_privacy_agreement_html: Me lektas e konsentas <a href="%{privacy_policy_path}" target="_blank">privatesguidilo</a>
+  author_attribution:
+    example_title: Exemplotexto
+    hint_html: Ka vu skribas novaji o blogartikli ne en Mastodon?
+    instructions: 'Certigez ke ca kodexo es en HTML di vua artiklo:'
+    more_from_html: Plu de %{name}
+    s_blog: Blogo di %{name}
+    then_instructions: Pose, adjuntez la domennomo di publikigo en la dessupra spaco.
+    title: Verkantoagnosko
   challenge:
     confirm: Avancez
     hint_html: "<strong>Guidilo:</strong> Ni ne demandos vua pasvorto itere til 1 horo."
@@ -1079,7 +1239,7 @@ io:
       x_months: "%{count}mo"
       x_seconds: "%{count}s"
   deletes:
-    challenge_not_passed: Informo quon vu insertis ne esas korekta
+    challenge_not_passed: Informo quan vu insertis es nekorekta
     confirm_password: Insertez nuna pasvorto por verifikar vua identeso
     confirm_username: Insertez vua uzantonomo por konfirmar procedo
     proceed: Efacez konto
@@ -1124,6 +1284,7 @@ io:
       your_appeal_rejected: Vua apelo refuzesis
   edit_profile:
     basic_information: Fundamentala informo
+    hint_html: "<strong>Personesigez quon personi vidas sur vua publika profilo e apud vua afishi.</strong>."
     other: Altra
   errors:
     '400': Demando quon vu sendis esas nevalida o malstrukturala.
@@ -1139,27 +1300,26 @@ io:
       content: Regreto, ma evas nefuncionas che nia latero.
       title: Ca pagino ne esas korekta
     '503': La pagino ne povas montresas pro tempala servilfalio.
-    noscript_html: Por uzar retsoftwaro di Mastodon, aktivigez JavaScript. Alternative, probez 1 de <a href="%{apps_path}">konciliebla softwari</a> por Mastodon por vua platformo.
   existing_username_validator:
     not_found: ne povas trovar lokala uzanti kun ta uzantonomo
     not_found_multiple: ne povas trovar %{usernames}
   exports:
     archive_takeout:
       date: Dato
-      download: Deschargez vua arkivo
-      hint_html: Vu povas demandar arkivo di vua <strong>posti e adchargita medii</strong>. Exportacita informi esos kun ActivityPub format, e lektebla da irga konciliebla softwaro. Vu povas demandar arkivo pos singla 7 dii.
+      download: Deskargar vua arkivo
+      hint_html: Vu povas demandar arkivo di vua <strong>afishi e adkargita audvidaji</strong>. Vu povas demandar arkivo pos singla 7 dii.
       in_progress: Kompilar vua arkivo...
       request: Demandez vua arkivo
       size: Grandeso
     blocks: Tu blokusas
-    bookmarks: Libromarki
+    bookmarks: Lektosigni
     csv: CSV
     domain_blocks: Domenobstrukti
     lists: Listi
     mutes: Vu silencigesas
-    storage: Konservado di kontenajo
+    storage: Audvidajkonservo
   featured_tags:
-    add_new: Insertez novo
+    add_new: Adjuntar novo
     errors:
       limit: Vu ja pinglizis la maxima nombro de hastagi
     hint_html: "<strong>Quo esas estelita hashtagi?</strong> Ol montresas eminente che vua publika profilo e povigas personi vidar vua publika posti partikulare kun ta hashtagi. Oli esas bona utensilo por jeretar kreiva agaji e longa projetaji."
@@ -1171,11 +1331,11 @@ io:
       public: Publika tempolinei
       thread: Konversi
     edit:
-      add_keyword: Insertez klefvorto
+      add_keyword: Adjuntar klefvorto
       keywords: Klefvorti
       statuses: Individuala posti
       statuses_hint_html: Ca filtrilo aplikesas a selektita posti ne segun kad oli parigesas kun basa klefvorti. <a href="%{path}">Kontrolez o efacez posti de la filtrilo</a>.
-      title: Modifikez filtrilo
+      title: Redaktar filtrilo
     errors:
       deprecated_api_multiple_keywords: Ca parametri ne povas chanjesar per ca softwaro pro quo oli efektigas plu kam 1 filtrilklefvorto. Uzez plu recenta softwaro o interretintervizajo.
       invalid_context: Nula o nevalida kuntexto donesis
@@ -1197,7 +1357,7 @@ io:
       title: Filtrili
     new:
       save: Salvez nova filtrilo
-      title: Insertez nova filtrilo
+      title: Adjuntar nova filtrilo
     statuses:
       back_to_filter: Retrovenez a filtrilo
       batch:
@@ -1240,7 +1400,7 @@ io:
     mismatched_types_warning: Semblas ke vu forsan selektis la nekorekta tipo por ca importaco, voluntez kontrolar itere.
     modes:
       merge: Kombinez
-      merge_long: Retenez displonebla rekordi e insertez novi
+      merge_long: Konservar existanta rekordi e adjuntar novi
       overwrite: Remplasez
       overwrite_long: Remplasez nuna rekordi per novi
     preface: Tu povas importacar kelka datumi, tal quala listi de omna homi quin tu sequas o blokusas, a tua konto di ca instaluro, per dosiero exportacita de altra instaluro.
@@ -1251,7 +1411,7 @@ io:
       scheduled: Projetita
       unconfirmed: Nekonfirmata
     status: Stando
-    success: Tua datumi esis senprobleme importacita ed esos traktita quale projetita
+    success: Vua datumi es senprobleme adkargita e esos traktita balde
     time_started: Komencita ye
     titles:
       blocking: Importacante konti blokusata
@@ -1266,14 +1426,14 @@ io:
       destructive: Blokusati e silencigati
     types:
       blocking: Listo de blokusiti
-      bookmarks: Libromarki
+      bookmarks: Lektosigni
       domain_blocking: Domenobstruktolisto
       following: Listo de sequati
       lists: Listi
       muting: Silenciglisto
-    upload: Kargar
+    upload: Adkargar
   invites:
-    delete: Deaktivigez
+    delete: Desebligar
     expired: Expiris
     expires_in:
       '1800': 30 minuti
@@ -1315,13 +1475,14 @@ io:
       title: Desabonez
   media_attachments:
     validations:
-      images_and_video: Ne povas atachar video a posto quo ja kontenar imaji
-      not_ready: Ne povas faili quo ankore procedigesas. Probez itere pose!
-      too_many: Ne povas atachar plu kam 4 faili
+      images_and_video: Ne povas addonar video ad afisho qua ja enhavas imaji
+      not_found: Audvidaji %{ids} es ne trovesis o ja addonesis ad altra afisho
+      not_ready: Ne povas addonar dosieri qua ankore ne finigas procedar.
+      too_many: Ne povas addonar plu kam 4 dosieri
   migrations:
     acct: Transferesis a
     cancel: Anulez ridirekto
-    cancel_explanation: Anular ridirekto riaktivigos vua nuna konto, ma ne riganos sequanti quo transferesis a ta konto.
+    cancel_explanation: Nuligar ridirekto riaktivigos vua nuna konto, ma ne riganos sequanti qua movigesis ad ta konto.
     cancelled_msg: Sucesoze anulis ridirekto.
     errors:
       already_moved: esas sama transferkonto
@@ -1344,7 +1505,7 @@ io:
       backreference_required: Nova konto mustas unesme ajustesar por dopreferar co
       before: 'Ante durar, lektez ca noti sorgeme:'
       cooldown: Pos transferesar, existas varttempo kande vu ne povas transferesar itere
-      disabled_account: Vua nuna konto ne divenos tote uzebla pose. Tamen, vu povas acesar informexportaco e anke riaktivigo.
+      disabled_account: Vua nuna konto ne esos tote uzebla pose.
       followers: Ca ago transferos omna sequanti de nuna konto a nova konto
       only_redirect_html: Alternative, vu povas <a href="%{path}">nur pozar ridirekto che vua profilo</a>.
       other_data: Altra informi ne transferesos automate
@@ -1382,7 +1543,7 @@ io:
       subject: "%{name} mencionis tu"
       title: Nova menciono
     poll:
-      subject: Votposto di %{name} finis
+      subject: Votinquesto da %{name} fineskis
     reblog:
       body: "%{name} repetis vua posto:"
       subject: "%{name} repetis vua posto"
@@ -1390,7 +1551,7 @@ io:
     status:
       subject: "%{name} nove postigis"
     update:
-      subject: "%{name} modifikis posto"
+      subject: "%{name} redaktis afisho"
   notifications:
     email_events_hint: 'Selektez eventi quon vu volas ganar avizi:'
   number:
@@ -1404,13 +1565,13 @@ io:
           thousand: K
           trillion: T
   otp_authentication:
-    code_hint: Insertez kodexo quo facesis da vua yurizsoftwaro por konfirmar
-    description_html: Se vu aktivigas <strong>2-faktoryurizo</strong> per yurizsoftwaro, eniro bezonos vu havar vua smartfono, quale facas fichi por vu por enirar.
-    enable: Aktivigez
-    instructions_html: "<strong>Skanez ca QR-kodexo a Google Authenticator o simila TOTP-softwaro en vua smartfono</strong>. Pos co, ta softwaro facos fichi quon vu bezonos insertar kande enirar."
+    code_hint: Insertez kodexo qua igesis da vua yurizaplikajo por konfirmar
+    description_html: Se vu ebligas <strong>dufaktora yurizo</strong> per yurizaplikajo, eniro bezonos vu havar vua telefonilo, quo igos fichi por vu enirar.
+    enable: Ebligar
+    instructions_html: "<strong>Skanez ca QR-kodexo ad Google Authenticator o simila TOTP-aplikajo per vua telefonilo</strong>."
     manual_instructions: 'Se vu ne povas skanar QR-kodexo e bezonas insertar manuala, subo esas textosekreto:'
     setup: Facez
-    wrong_code: Insertita kodexo esas nevalida! Ka serviltempo e aparattempo esas korekta?
+    wrong_code: Insertita kodexo es nevalida!
   pagination:
     newer: Nova
     next: Sequanta
@@ -1419,24 +1580,28 @@ io:
     truncate: "&hellip;"
   polls:
     errors:
-      already_voted: Vu ja votis che ca votposto
-      duplicate_options: havas duplikatkozi
-      duration_too_long: esas tro distanta en futuro
+      already_voted: Vu ja votis sur ca votinquesto
+      duplicate_options: enhavas duopliga aji
+      duration_too_long: esas tro desproxima en la estonteso
       duration_too_short: es tro balde
-      expired: Votposto ja finis
+      expired: La votinquesto ja fineskis
       invalid_choice: Selektita votselektajo ne existas
-      over_character_limit: ne povas esar plu longa kam %{max} literi por sing
+      over_character_limit: ne povas esar plu longa kam %{max} literi por singlo
       self_vote: On ne povas votar en sua propra inquesti
-      too_few_options: mustas havar kozi
-      too_many_options: ne povas havar plu kam %{max} kozi
+      too_few_options: mustas havar plu kam un ajo
+      too_many_options: ne povas enhavar plu kam %{max} aji
   preferences:
     other: Altra
-    posting_defaults: Originala postoopcioni
+    posting_defaults: Originala afishago
     public_timelines: Publika tempolinei
   privacy:
+    hint_html: "<strong>Personesigez quale vu volas vua profilo e vua afishi trovesis.</strong> Instante kontrolez ca preferaji por certigar ke ili aptas segun vu."
     privacy: Privateso
+    privacy_hint_html: Selektar quale vu volas montrar por altra personi.
     reach: Atingo
+    reach_hint_html: Selektar ka vu volas deskovresar e sequesar da nova personi. Ka vu volas altra personi vidar vu en ilia sequrekomendi?
     search: Sercho
+    search_hint_html: Selektar quale vu volas trovesar. Ka vu volas personi ne ek Mastodon trovar vua profilo kande serchar la interreto?
     title: Privateso e atingo
   privacy_policy:
     title: Privatesguidilo
@@ -1528,7 +1693,7 @@ io:
     view_authentication_history: Videz yurizeshistorio di vua konto
   settings:
     account: Konto
-    account_settings: Kontoopcioni
+    account_settings: Kontopreferaji
     aliases: Kontoaltnomi
     appearance: Aspekto
     authorized_apps: Yurizita apliki
@@ -1547,12 +1712,15 @@ io:
     strikes: Jerstreki
     two_factor_authentication: Dufaktora autentikigo
     webauthn_authentication: Sekuresklefi
+  severed_relationships:
+    download: Deskargo (%{count})
+    preamble: On eble desganas sequati e sequanti kande on blokusas domeno o kande ona administranti decidas blokusar deslokala servilo.
   statuses:
     attached:
       audio:
-        one: "%{count} audio"
-        other: "%{count} audii"
-      description: 'Atachajo: %{attached}'
+        one: "%{count} sono"
+        other: "%{count} soni"
+      description: 'Addonajita: %{attached}'
       image:
         one: "%{count} imajo"
         other: "%{count} imaji"
@@ -1565,14 +1733,14 @@ io:
     disallowed_hashtags:
       one: 'kontenas nepermisita hashtago: %{tags}'
       other: 'kontenas nepermisita hashtagi: %{tags}'
-    edited_at_html: Modifikesis ye %{date}
+    edited_at_html: Redaktesis ye %{date}
     errors:
       in_reply_not_found: Posto quon vu probas respondar semblas ne existas.
     over_character_limit: limito de %{max} signi ecesita
     pin_errors:
-      direct: Posti quo povas videsar nur mencionita uzanti ne povas pinglagesar
-      limit: Vu ja pinglagis maxima posti
-      ownership: Posto di altra persono ne povas pinglagesar
+      direct: Afishi qua nur videsebla ad mencionita uzanti ne povas adpinglesar
+      limit: Vu ja adpinglis la maxima afishquanto
+      ownership: Afisho di altra persono ne povas adpinglesar
       reblog: Repeto ne povas pinglizesar
     title: '%{name}: "%{quote}"'
     visibilities:
@@ -1594,16 +1762,16 @@ io:
     interaction_exceptions_explanation: Notale, ne existas garantio ke posti efacesos se oli iras sub la favoratala o repetala solii pos iras super oli.
     keep_direct: Retenez direta mesaji
     keep_direct_hint: Ne efacas irga vua direta mesaji
-    keep_media: Retenez posti kun mediiatachaji
-    keep_media_hint: Ne efacas irga vua posti quo havas mediiatachaji
-    keep_pinned: Retenez pinglagita posti
-    keep_pinned_hint: Ne efacas irga vua pinglagita posti
-    keep_polls: Retenez votposti
-    keep_polls_hint: Ne efacas irga vua votposti
-    keep_self_bookmark: Retenez posti quon vu libromarkizis
-    keep_self_bookmark_hint: Ne efacas vua sua posti se vu libromarkizis li
-    keep_self_fav: Retenez posti quon vu favorizis
-    keep_self_fav_hint: Ne efacas vua sua posti se vu favorizis li
+    keep_media: Konservar afishi kun audvidajaddonaji
+    keep_media_hint: Ne forigas irga vua afishi qua havas audvidajaddonaji
+    keep_pinned: Konservar adpinglita afishi
+    keep_pinned_hint: Ne forigas irga vua adpinglita afishi
+    keep_polls: Konservar votinquesti
+    keep_polls_hint: Ne forigas irga vua votinquesti
+    keep_self_bookmark: Konservar afishi quan vu lektosignis
+    keep_self_bookmark_hint: Ne forigas vua sua afishi se vu lektosignis ili
+    keep_self_fav: Konservar afishi quan vu stelumis
+    keep_self_fav_hint: Ne forigas vua sua afishi se vu stelumis ili
     min_age:
       '1209600': 2 semani
       '15778476': 6 monati
@@ -1614,7 +1782,7 @@ io:
       '63113904': 2 yari
       '7889238': 3 monati
     min_age_label: Oldeslimito
-    min_favs: Retenez favorizita posti mine
+    min_favs: Konservar stelumita afishi mine
     min_favs_hint: Ne efacas irgo ek vua posti qua havas adminime ca quanto de favoriziti. Restez vakua por efacar posti senegarde la nombro de favoriziti
     min_reblogs: Retenez posti repetita adminime
     min_reblogs_hint: Ne efacas irgo ek vua posti qua havas adminime ca quanto de repeti. Restez vakua por efacar posti senegarde la nombro de repeti
@@ -1635,12 +1803,12 @@ io:
       month: "%b %Y"
       time: "%H:%M"
   two_factor_authentication:
-    add: Insertez
+    add: Adjuntar
     disable: Extingar
-    disabled_success: 2-faktoryurizeso sucesoze desaktivigesas
-    edit: Modifikez
-    enabled: 2-faktoryurizeso aktivigesas
-    enabled_success: 2-faktoryurizeso sucesoze aktivigesas
+    disabled_success: Dufaktora yurizo sucese desebligesis
+    edit: Redaktar
+    enabled: Dufaktora yurizo es ebligita
+    enabled_success: Dufaktora yurizo sucese ebligesis
     generate_recovery_codes: Generate Recovery Codes
     lost_recovery_codes: Rigankodexi povigas vu riganas aceso a vua konto se vu perdas vua smartfono. Se vu perdas vua rigankodexi, vu povas rifacar hike. Vua olda rigankodexi nevalidigesos.
     methods: 2-faktormetodi
@@ -1659,13 +1827,16 @@ io:
       subject: Vua apelo de %{date} refuzesis
       title: Apelo refuzesis
     backup_ready:
-      subject: Vua arkivo pronte deschargebla
+      extra: Ol es pronta por deskargo!
+      subject: Vua arkivo es pronta por deskargo
       title: Arkivekpreno
+    failed_2fa:
+      further_actions_html: Se ol ne es vu, ni rekomendas ke vu %{action} quik pro ke ol eble maligita.
     suspicious_sign_in:
       change_password: chanjez vua pasvorto
       details: 'Co esas detali di eniro:'
       explanation: Ni deskovris eniro a vua konto de nova adreso IP.
-      further_actions_html: Se co ne agesis da vu, ni rekomendas ke vu %{action} quik e aktivigas 2-faktoryurizo por sekurigar vua konto.
+      further_actions_html: Se ol ne es vu, ni rekomendas ke vu %{action} quik e ebligas dufaktoryurizo por sekurigar vua konto.
       subject: Vua konto acesesis de nova adreso IP
       title: Nova eniro
     warning:
@@ -1676,11 +1847,11 @@ io:
         violation: Kontenajo nesequas ca komunitatguidili
       explanation:
         delete_statuses: Ol trovesis ke kelka vua posti nesequas komunitatguidilo e do efacesis da jereri di %{instance}.
-        disable: Vu ne povas uzar vua konto pluse, ma vua profilo e altra informi restas bone. Vu povas demandar kopiur di vua informi, chanjar kontoopcioni e efacar vua konto.
-        mark_statuses_as_sensitive: Kelka vua posti markizesis quale sentoza da jereri di %{instance}. Do, personi bezonos kliktar medii en posti ante prevido montresas. Vu povas markizar medii quale sentoza per su kande postar en futuro.
-        sensitive: Pos co, omna vua adchargita mediifaili markizesos quale sentoza e celesos dop kliktoaverto.
+        disable: Vu ne povas uzar vua konto plue, ma vua profilo e altra informi restas kune.
+        mark_statuses_as_sensitive: Kelka vua afishi markesis quale trublema da administranti di %{instance}. Vu povas markar audvidaji quale sentema per su kande afishar en la estonteso.
+        sensitive: Pos co, omna vua adkargita audvidajdosieri markesos quale trublema e celesos dop klikaverto.
         silence: Vu povas ankore uzar vua konto ma nur personi quo ja sequis vu vidos vua posti en ca servilo, e vu forsan neinkluzesas de diversa deskovrotraiti. Tamen, altra personi povas ankore manuala sequar vu.
-        suspend: Vu ne povas uzar vua konto pluse, e vua profilo e altra informi ne esas acesebla pluse. Vu povas ankore enirar por demandar kopiur di vua informi til informi komplete efacesas pos cirkum 30 dii, ma ni retenos kelka bazala informi por preventar vu evitar restrikto.
+        suspend: Vu ne povas uzar vua konto plue, e vua profilo e altra informi ne es adirebla plue.
       reason: 'Motivo:'
       statuses: 'Citita posti:'
       subject:
@@ -1700,7 +1871,12 @@ io:
         silence: Konto limitizesis
         suspend: Konto restriktigesis
     welcome:
+      apps_ios_action: Deskargez sur la aplikajbutiko
+      apps_step: Deskargez nia oficala aplikaji.
       explanation: Subo esas guidilo por helpar vu komencar
+      feature_creativity: Mastodon subtenas sono, video e imajafishi, adirpriskribi, votinquesti, enhavajaverti, anima imaji, personesigita emocimaji, imajettranchregulado e plu multo por helpar on expresar su enrete.
+      feature_moderation: Mastodon adportas decidebleso ad on. Adeskez servilo kun reguli quan vu akordas o gastigas vua sua kozo.
+      post_step: Salutez per texto, fotografi, videi o votinquesti.
       subject: Bonveno a Mastodon
       title: Bonveno, %{name}!
   users:
@@ -1714,19 +1890,19 @@ io:
     verification: Verifikeso
     verified_links: Vua ligili verifikata
   webauthn_credentials:
-    add: Insertez nova sekuresklefo
+    add: Adjuntar nova sekuresklefo
     create:
-      error: Existas problemo kande insertar vua sekuresklefo. Probez itere.
-      success: Vua sekuresklefo sucesoze insertesas.
+      error: Esas problemo kande adjuntar vua sekuresklefo.
+      success: Vua sekuresklefo sucese adjuntesis.
     delete: Efacez
     delete_confirmation: Ka vu certe volas efacar ca sekuresklefo?
-    description_html: Se vu aktivigas <strong>sekuresklefyurizo</strong>, eniro bezonos vu uzar 1 de vua sekuresklefi.
+    description_html: Se vu ebligas <strong>sekuresklefyurizo</strong>, eniro bezonos vu uzar un ek vua sekuresklefi.
     destroy:
       error: Existas problemo kande efacar vua sekuresklefo. Probez itere.
       success: Vua sekuresklefo sucesoze efacesis.
     invalid_credential: Nevalida sekuresklefo
     nickname_hint: Insertez nometo di vua nova sekuresklefo
-    not_enabled: Vu ne aktivigesas WebAuthn til nun
+    not_enabled: Vu ne ebligis WebAuthn til nun
     not_supported: Ca vidilo ne suportas sekuresklefi
-    otp_required: Por uzar sekuresklefi, aktivigez 2-faktoryurizeso unesme.
+    otp_required: Por uzar sekuresklefi, ebligez dufaktora yurizo unesme.
     registered_on: Registris ye %{date}
diff --git a/config/locales/is.yml b/config/locales/is.yml
index c4172f238d..0516fa4eb8 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -309,6 +309,7 @@ is:
       title: Atvikaskrá
       unavailable_instance: "(heiti léns ekki tiltækt)"
     announcements:
+      back: Til baka í auglýsingar
       destroyed_msg: Það tókst að eyða auglýsingunni!
       edit:
         title: Breyta auglýsingu
@@ -317,6 +318,10 @@ is:
       new:
         create: Búa til auglýsingu
         title: Ný auglýsing
+      preview:
+        disclaimer: Þar sem notendur geta ekki afþakkað þær ætti aðeins að nota tilkynningar í tölvupósti fyrir mikilvægar upplýsingar á borð við persónuleg gagnabrot eða lokanir á netþjónum.
+        explanation_html: 'Tölvupósturinn verður sendur til <strong>%{display_count} notenda</strong>. Eftirfarandi texti verður í meginmáli póstsins:'
+        title: Forskoða tilkynninguna
       publish: Birta
       published_msg: Það tókst að birta auglýsinguna!
       scheduled_for: Áætlað %{time}
@@ -475,6 +480,36 @@ is:
       new:
         title: Flytja inn útilokanir léna
       no_file: Engin skrá valin
+    fasp:
+      debug:
+        callbacks:
+          created_at: Búið til
+          delete: Eyða
+          ip: IP-vistfang
+          request_body: Biðja um meginmál
+          title: Villuleita bakrakningar (callbacks)
+      providers:
+        active: Virkt
+        base_url: Grunnslóð
+        callback: Bakrakning (callback)
+        delete: Eyða
+        edit: Breyta þjónustuaðila
+        finish_registration: Ljúka skráningu
+        name: Nafn
+        providers: Þjónustuaðilar
+        public_key_fingerprint: Fingrafar dreifilykils
+        registration_requested: Beðið um skráningu
+        registrations:
+          confirm: Staðfesta
+          description: Þú fékkst skráningu frá FASP-þjónustuaðila. Hafnaðu þessu ef þú baðst ekki um þetta. Ef þú baðst um þetta, skaltu bera vandlega saman nöfn og fingraför lykla áður en þú staðfestir skráninguna.
+          reject: Hafna
+          title: Staðfesta FASP-skráningu
+        save: Vista
+        select_capabilities: Veldu eiginleika
+        sign_in: Skrá inn
+        status: Staða
+        title: Fediverse Auxiliary Service Providers þjónustuaðilar
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Að fylgja meðmælum hjálpar nýjum notendum að finna áhugavert efni á einfaldan máta</strong>. Þegar notandi hefur ekki átt í nægilegum samskiptum við aðra til að vera farinn að móta sér skoðanir á hverju hann vill fylgjast með, er mælt með að fylgjast með þessum aðgöngum. Þeir eru endurreiknaðir daglega út frá blöndu þeirra aðganga sem eru með hvað mestri þáttöku í umræðum og mesta fylgjendafjölda út frá hverju tungumáli."
       language: Fyrir tungumálið
@@ -941,6 +976,7 @@ is:
         chance_to_review_html: "<strong>Sjálfvirkt útbúnu þjónustuskilmálarnir verða ekki birtir sjálfkrafa.</strong> Þú munt geta yfirfarið textann. Fylltu inn nauðsynlegar upplýsingar til að halda áfram to proceed."
         explanation_html: Sniðmátið fyrir þjónustuskilmála sem hér fylgir er einungis hugsat til upplýsingar og ætti ekki að taska sem lögfræðilegt álit varðandi hin ýmsu efni. Þú skalt endilega ráðfæra þig við löglærða í þínu umdæmi varðandi þau atriði sem eiga við um stöðu þína auk annarra spurninga um lögfræðileg mál sem þú gætir haft.
         title: Uppsetning þjónustuskilmála
+      going_live_on_html: Í beinni, þann %{date}
       history: Ferill
       live: Í beinni
       no_history: Það eru engar breytingar ennþá á þjónustuskilmálunum.
@@ -1908,6 +1944,10 @@ is:
     recovery_instructions_html: Ef þú tapar símanum þínum geturðu notað einn af endurheimtukóðunum hér fyrir neðan til að fá aftur samband við notandaaðganginn þinn. <strong>Geymdu endurheimtukóðana á öruggum stað</strong>. Sem dæmi gætirðu prentað þá út og geymt með öðrum mikilvægum skjölum.
     webauthn: Öryggislyklar
   user_mailer:
+    announcement_published:
+      description: 'Stjórnendur %{domain} eru að senda frá sér yfirlýsingu:'
+      subject: Auglýsing vegna þjónustu
+      title: Auglýsing vegna þjónustu %{domain}
     appeal_approved:
       action: Stillingar notandaaðgangs
       explanation: Áfrýjun refsingarinnar gagnvart aðgangnum þínum þann %{strike_date} sem þú sendir inn þann %{appeal_date} hefur verið samþykkt. Notandaaðgangurinn þinn er aftur í góðu lagi.
@@ -1940,8 +1980,8 @@ is:
     terms_of_service_changed:
       agreement: Með því að halda áfram að nota %{domain}, ert þú þar með að samþykkja þessa skilmála. Ef þú ert ósammála þessum uppfærðu skilmálum, geturðu hvenær sem er sagt upp samþykki þínu gagnvart %{domain} með því að eyða notandaaðgangi þínum.
       changelog: 'Í stuttu máli er það þetta sem þessi uppfærsla þýðir fyrir þig:'
-      description: 'Þú færð þennan tölvupóst vegna þess að við erum að gera nokkrar breytingar á þjónustuskilmálum okkar á %{domain}. Við hvetjum þig til að yfirfara uppfærðu skilmálana í heild hér:'
-      description_html: Þú færð þennan tölvupóst vegna þess að við erum að gera nokkrar breytingar á þjónustuskilmálum okkar á %{domain}. Við hvetjum þig til að yfirfara <a href="%{path}" target="_blank">uppfærðu skilmálana í heild hér:</a>.
+      description: 'Þú ert að fá þennan tölvupóst vegna þess að við erum að gera breytingar á þjónustuskilmálum á %{domain}. Þessar breytingar taka gildi þann %{date}. Við hvetjum þig til að kynna þér þessar breytingar hér:'
+      description_html: Þú ert að fá þennan tölvupóst vegna þess að við erum að gera breytingar á þjónustuskilmálum á %{domain}. Þessar breytingar taka gildi þann <strong>%{date}</strong>. Við hvetjum þig til að kynna þér <a href="%{path}" target="_blank">þessar breytingar hér</a>.
       sign_off: "%{domain}-teymið"
       subject: Breytingar á þjónustuskilmálum okkar
       subtitle: Þjónustuskilmálar eru að breytast á %{domain}
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 734860f3bb..95096b07c8 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -309,6 +309,7 @@ it:
       title: Registro di controllo
       unavailable_instance: "(nome di dominio non disponibile)"
     announcements:
+      back: Torna agli annunci
       destroyed_msg: Annuncio eliminato!
       edit:
         title: Modifica annuncio
@@ -317,6 +318,9 @@ it:
       new:
         create: Crea annuncio
         title: Nuovo annuncio
+      preview:
+        explanation_html: 'L''e-mail verrà inviata a <strong>%{display_count} utenti</strong>. Il seguente testo sarà incluso nell''e-mail:'
+        title: Anteprima della notifica dell'annuncio
       publish: Pubblica
       published_msg: Annuncio pubblicato!
       scheduled_for: Programmato per %{time}
@@ -475,6 +479,36 @@ it:
       new:
         title: Importare i blocchi di dominio
       no_file: Nessun file selezionato
+    fasp:
+      debug:
+        callbacks:
+          created_at: Creato il
+          delete: Cancella
+          ip: Indirizzo IP
+          request_body: Request body
+          title: Debug Callbacks
+      providers:
+        active: Attivo
+        base_url: Url di base
+        callback: Callback
+        delete: Cancella
+        edit: Modifica Provider
+        finish_registration: Registrazione terminata
+        name: Nome
+        providers: Providers
+        public_key_fingerprint: Chiave pubblica fingerprint
+        registration_requested: Registrazione richiesta
+        registrations:
+          confirm: Conferma
+          description: Hai ricevuto una registrazione da un FASP. Rifiutala se non l'hai avviata tu. Se l'hai avviata tu, confronta attentamente nome e impronta della chiave prima di confermare la registrazione.
+          reject: Rifiuta
+          title: Conferma registrazione FASP
+        save: Salva
+        select_capabilities: Seleziona Capacità
+        sign_in: Connettiti
+        status: Stato
+        title: Fornitori di Servizi Ausiliari per il Fediverso
+      title: FASP
     follow_recommendations:
       description_html: "<strong>I consigli su chi seguire aiutano i nuovi utenti a trovare rapidamente dei contenuti interessanti</strong>. Quando un utente non ha interagito abbastanza con altri per avere dei consigli personalizzati, vengono consigliati questi account. Sono ricalcolati ogni giorno da un misto di account con le più alte interazioni recenti e con il maggior numero di seguaci locali per una data lingua."
       language: Per lingua
@@ -939,6 +973,7 @@ it:
         chance_to_review_html: "<strong>I termini di servizio generati non verranno pubblicati automaticamente.</strong> Avrai la possibilità di esaminare i risultati. Si prega di inserire i dettagli necessari per procedere."
         explanation_html: Il modello di termini di servizio fornito è solo a scopo informativo e non deve essere interpretato come consulenza legale su alcun argomento. Si prega di consultare il proprio consulente legale, in merito alla propria situazione e alle specifiche questioni legali che si hanno.
         title: Impostazione dei Termini di Servizio
+      going_live_on_html: In vigore il %{date}
       history: Cronologia
       live: In uso
       no_history: Non sono ancora state registrate modifiche ai termini di servizio.
@@ -1906,6 +1941,10 @@ it:
     recovery_instructions_html: Se perdi il telefono, puoi usare uno dei codici di recupero qui sotto per riottenere l'accesso al tuo account. <strong>Conserva i codici di recupero in un posto sicuro</strong>. Ad esempio puoi stamparli e conservarli insieme ad altri documenti importanti.
     webauthn: Chiavi di sicurezza
   user_mailer:
+    announcement_published:
+      description: 'Gli amministratori di %{domain} stanno facendo un annuncio:'
+      subject: Annuncio di servizio
+      title: Annuncio di servizio da %{domain}
     appeal_approved:
       action: Impostazioni account
       explanation: L'appello della sanzione contro il tuo account del %{strike_date} che hai inviato il %{appeal_date} è stato approvato. Il tuo account ha riottenuto la buona reputazione.
@@ -1938,8 +1977,8 @@ it:
     terms_of_service_changed:
       agreement: Continuando a usare %{domain}, accetti questi termini. Se non sei d'accordo con i termini aggiornati, puoi terminare il tuo accordo con %{domain} in qualsiasi momento eliminando il tuo account.
       changelog: 'Ecco, in sintesi, cosa significa per te questo aggiornamento:'
-      description: 'Stai ricevendo questa e-mail perché stiamo apportando alcune modifiche ai nostri termini di servizio su %{domain}. Ti invitiamo a leggere i termini aggiornati per intero qui:'
-      description_html: Stai ricevendo questa e-mail perché stiamo apportando alcune modifiche ai nostri termini di servizio su %{domain}. Ti invitiamo a leggere i <a href="%{path}" target="_blank">termini aggiornati per intero qui</a>.
+      description: 'Stai ricevendo questa e-mail perché stiamo apportando alcune modifiche ai nostri termini di servizio su %{domain}. Questi aggiornamenti entreranno in vigore il %{date}. Ti invitiamo a leggere i termini aggiornati per intero qui:'
+      description_html: Stai ricevendo questa e-mail perché stiamo apportando alcune modifiche ai nostri termini di servizio su %{domain}. Questi aggiornamenti entreranno in vigore il <strong>%{date}</strong>. Ti invitiamo a leggere i <a href="%{path}" target="_blank">termini aggiornati per intero qui</a>.
       sign_off: Il team di %{domain}
       subject: Aggiornamenti ai nostri termini di servizio
       subtitle: I termini di servizio di %{domain} stanno cambiando
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index b752bd3879..73de14abeb 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -517,6 +517,17 @@ ja:
       new:
         title: ドメインブロックをインポート
       no_file: ファイルが選択されていません
+    fasp:
+      debug:
+        callbacks:
+          delete: 削除
+          ip: IPアドレス
+      providers:
+        delete: 削除
+        name: 名前
+        providers: プロバイダ
+        public_key_fingerprint: 公開キーのフィンガープリント
+        save: 保存
     follow_recommendations:
       description_html: "<strong>おすすめフォローは、新規ユーザーが興味のあるコンテンツをすばやく見つけるのに役立ちます。</strong>ユーザーが他のユーザーとの交流を十分にしていない場合、パーソナライズされたおすすめフォローを生成する代わりに、これらのアカウントが表示されます。最近のエンゲージメントが最も高いアカウントと、特定の言語のローカルフォロワー数が最も多いアカウントを組み合わせて、毎日再計算されます。"
       language: 言語
@@ -2252,8 +2263,6 @@ ja:
     terms_of_service_changed:
       agreement: "%{domain} を引き続き使用することで、これらの条件に同意したことになります。更新された条件に同意しない場合は、アカウントを削除することでいつでも %{domain} との契約を終了することができます。"
       changelog: 一目で分かる、この更新があなたにとって意味することは次の通りです:
-      description: このメールを受け取っているのは、%{domain} の利用規約にいくつかの変更を加えているためです。更新された利用規約をこちらで全てご確認いただくことをお勧めします:
-      description_html: このメールを受け取っているのは、%{domain} の利用規約にいくつかの変更を加えているためです。<a href="%{path}" target="_blank">こちらで更新された利用規約を全てご確認いただくことをお勧めします</a>。
       sign_off: "%{domain} チーム"
       subject: 利用規約の更新
       subtitle: "%{domain} の利用規約が変更されています"
diff --git a/config/locales/kab.yml b/config/locales/kab.yml
index 11cc8b3318..85a88be7bb 100644
--- a/config/locales/kab.yml
+++ b/config/locales/kab.yml
@@ -71,7 +71,7 @@ kab:
         active: Yermed
         all: Akk
         pending: Yettraǧu
-        suspended: Yeḥbes
+        suspended: Yettwaḥbes
         title: Aseɣyed
       moderation_notes: Tamawin n useɣyed
       most_recent_activity: Armud aneggaru
@@ -109,7 +109,7 @@ kab:
       silenced: Yettwasgugem
       statuses: Tisuffaɣ
       subscribe: Jerred
-      suspended: Yeḥbes
+      suspended: Yettwaḥbes
       title: Imiḍanen
       unconfirmed_email: Imayl ur yettwasentem ara
       undo_silenced: Kkes asgugem
@@ -268,6 +268,15 @@ kab:
       no_file: Ula d yiwen ufaylu ma yettwafran
     export_domain_blocks:
       no_file: Ulac afaylu yettwafernen
+    fasp:
+      debug:
+        callbacks:
+          delete: Kkes
+          ip: Tansa IP
+      providers:
+        delete: Kkes
+        save: Sekles
+      title: FASP
     follow_recommendations:
       language: I tutlayt
       status: Addad
@@ -378,6 +387,7 @@ kab:
       everyone: Tisirag timezwura
       privileges:
         administrator: Anedbal
+        view_dashboard: Timẓriwt n tfelwit
     rules:
       add_new: Rnu alugen
       delete: Kkes
@@ -391,6 +401,7 @@ kab:
         title: Udem
       discovery:
         profile_directory: Akaram n imaɣnuten
+        title: Asnirem
         trends: Ayen mucaɛen
       domain_blocks:
         all: I medden akk
@@ -431,11 +442,18 @@ kab:
       software_version_patch_check:
         action: Wali ileqqman yellan
     tags:
+      name: Isem
+      newest: Amaynut
+      oldest: Aqbur
       search: Anadi
       title: Ihacṭagen
     terms_of_service:
+      changelog: Amaynut
       draft: Arewway
+      history: Amazray
       publish: Asuffeɣ
+      save_draft: Sekles arewway
+      title: Tiwtilin n useqdec
     title: Tadbelt
     trends:
       allow: Sireg
@@ -489,7 +507,7 @@ kab:
     delete_account: Kkes amiḍan
     description:
       prefix_invited_by_user: "@%{name} inced-ik·ikem ad ternuḍ ɣer uqeddac-a n Mastodon!"
-      prefix_sign_up: Zeddi di Maṣṭudun assa!
+      prefix_sign_up: Zeddi di Maṣṭudun ass-a!
       suffix: S umiḍan, tzemreḍ ad tḍefreḍ imdanen, ad d-tessufɣeḍ tisuffaɣ d wembadal n yiznan akked yiseqdacen n yal aqeddac Mastodon d wayen-nniḍen!
     didnt_get_confirmation: Ur d-teṭṭifeḍ ara aseɣwen n usentem ?
     dont_have_your_security_key: Ulac ɣur-k·m tasarut-ik·im n tɣellist?
@@ -597,7 +615,7 @@ kab:
   filters:
     contexts:
       account: Imeɣna
-      notifications: Alɣuten
+      notifications: Ilɣa
       thread: Idiwenniyen
     edit:
       add_keyword: Rnu awal tasarut
@@ -701,6 +719,7 @@ kab:
         units:
           billion: AṬ
           million: A
+          thousand: GM
           trillion: Am
   otp_authentication:
     enable: Rmed
@@ -746,6 +765,7 @@ kab:
       electron: Electron
       firefox: Firefox
       generic: Iminig arusin
+      huawei_browser: Iminig n Huawei
       ie: Internet Explorer
       micro_messenger: MicroMessenger
       nokia: Iminig Nokia S40 Ovi
@@ -754,6 +774,7 @@ kab:
       phantom_js: PhantomJS
       qq: Iminig QQ
       safari: Safari
+      uc_browser: UC Browser
       unknown_browser: Iminig arussin
       weibo: Weibo
     current_session: Tiɣimit tamirant
@@ -789,7 +810,7 @@ kab:
     import: Kter
     import_and_export: Taktert d usifeḍ
     migrate: Tunigin n umiḍan
-    notifications: Alɣuten s imayl
+    notifications: Ilɣa s imayl
     preferences: Imenyafen
     profile: Ameɣnu
     relationships: Imeḍfaṛen akked wid i teṭṭafaṛeḍ
@@ -829,6 +850,8 @@ kab:
       '7889238': 3 n wayyuren
   stream_entries:
     sensitive_content: Agbur amḥulfu
+  terms_of_service:
+    title: Tiwtilin n useqdec
   themes:
     contrast: Maṣṭudun (agnil awriran)
     default: Maṣṭudun (Aberkan)
@@ -851,9 +874,12 @@ kab:
   user_mailer:
     appeal_approved:
       action: Iɣewwaṛen n umiḍan
+    terms_of_service_changed:
+      sign_off: Agraw n %{domain}
     warning:
       categories:
         spam: Aspam
+      reason: 'Taɣẓint:'
       title:
         disable: Amiḍan i igersen
         none: Ɣur-wat
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 5405cabd9b..1ce5ef2da9 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -194,7 +194,7 @@ ko:
         destroy_domain_allow: 도메인 허용 삭제
         destroy_domain_block: 도메인 차단 삭제
         destroy_email_domain_block: 이메일 도메인 차단 삭제
-        destroy_instance: 도메인 퍼지하기
+        destroy_instance: 도메인 퍼지
         destroy_ip_block: IP 규칙 삭제
         destroy_relay: 릴레이 삭제
         destroy_status: 게시물 삭제
@@ -306,6 +306,7 @@ ko:
       title: 감사 로그
       unavailable_instance: "(도메인네임 사용불가)"
     announcements:
+      back: 공지사항으로 돌아가기
       destroyed_msg: 공지가 성공적으로 삭제되었습니다!
       edit:
         title: 공지사항 편집
@@ -314,6 +315,10 @@ ko:
       new:
         create: 공지사항 생성
         title: 새 공지사항
+      preview:
+        disclaimer: 사용자들은 수신설정을 끌 수 없기 때문에 이메일 알림은 개인정보 유출이나 서버 종료와 같은 중요한 공지사항에만 사용해야 합니다.
+        explanation_html: "<strong>%{display_count} 명의 사용자</strong>에게 이메일이 발송됩니다. 다음 내용이 이메일에 포함됩니다:"
+        title: 공지사항 알림 미리보기
       publish: 게시
       published_msg: 공지사항이 성공적으로 발행되었습니다!
       scheduled_for: "%{time}에 예약됨"
@@ -469,6 +474,36 @@ ko:
       new:
         title: 도메인 차단 불러오기
       no_file: 선택된 파일이 없습니다
+    fasp:
+      debug:
+        callbacks:
+          created_at: 생성일
+          delete: 삭제
+          ip: IP 주소
+          request_body: 요청 바디
+          title: 디버그 콜백
+      providers:
+        active: 활성
+        base_url: Base URL
+        callback: 콜백
+        delete: 삭제
+        edit: 제공자 편집
+        finish_registration: 등록 완료
+        name: 이름
+        providers: 제공자
+        public_key_fingerprint: 공개키 핑거프린트
+        registration_requested: 가입 요청됨
+        registrations:
+          confirm: 확인
+          description: FASP로부터 등록을 받았습니다. 본인이 시작하지 않았다면 거부하세요. 본인이 시작했다면 등록을 확인하기 전에 이름과 키 핑거프린트를 주의 깊게 비교하세요.
+          reject: 거부
+          title: FASP 가입 확인
+        save: 저장
+        select_capabilities: 권한 선택
+        sign_in: 가입
+        status: 상태
+        title: 연합우주 보조 서비스 제공자
+      title: FASP
     follow_recommendations:
       description_html: "<strong>팔로우 추천은 새 사용자들이 관심 가는 콘텐트를 빠르게 찾을 수 있도록 도와줍니다</strong>. 사용자가 개인화 된 팔로우 추천이 만들어지기 위한 충분한 상호작용을 하지 않은 경우, 이 계정들이 대신 추천 됩니다. 이들은 해당 언어에 대해 많은 관심을 갖거나 많은 로컬 팔로워를 가지고 있는 계정들을 섞어서 날마다 다시 계산 됩니다."
       language: 언어 필터
@@ -927,6 +962,7 @@ ko:
         chance_to_review_html: "<strong>생성된 이용 약관은 자동으로 게시되지 않을 것입니다.</strong> 결과를 확인할 기회가 있습니다. 진행하려면 필요한 정보들을 입력하세요."
         explanation_html: 제공되는 이용약관 틀은 정보 제공만을 목적으로 하며 법률 조언으로 해석하면 안 됩니다. 귀하의 상황에 맞는 법률 자문을 받아주시기 바랍니다.
         title: 이용 약관 설정
+      going_live_on_html: "%{date} 시행중"
       history: 역사
       live: 활성
       no_history: 기록된 이용약관 변경이 아직 없습니다.
@@ -1673,7 +1709,7 @@ ko:
     over_total_limit: 예약 게시물 제한 %{limit}을 초과합니다
     too_soon: 미래의 날짜여야 합니다
   self_destruct:
-    lead_html: 안타깝게도,  <strong>%{domain}</strong> 도메인을 영구히 폐쇄합니다. 이곳의 계정을 가졌다면, 이제 이용할 수 없으며, 당분간 백업 데이터를 요청할 수 있습니다.
+    lead_html: 안타깝게도, <strong>%{domain}</strong>은 영구적으로 폐쇄됩니다. 이곳의 계정을 가지고 있었다면, 이제 이용할 수 없지만 백업 데이터는 요청할 수 있습니다.
     title: 이 서버는 폐쇄중입니다
   sessions:
     activity: 최근 활동
@@ -1754,7 +1790,7 @@ ko:
     lost_followers: 잃은 팔로워
     lost_follows: 잃은 팔로우
     preamble: 내가 도메인을 차단하거나 중재진이 다른 서버를 정지하기로 결정했다면 내 팔로우와 팔로워를 잃게 됩니다. 그런 일이 일어났다면 그로 인해 단절된 관계들의 목록을 다운로드 받아 확인하고 다른 서버에서 불러올 수 있습니다.
-    purged: 이 서버의 정보는 관리자가 퍼지하였습니다.
+    purged: 이 서버에 대한 정보는 관리자에 의해 제거되었습니다.
     type: 이벤트
   statuses:
     attached:
@@ -1863,6 +1899,10 @@ ko:
     recovery_instructions_html: 휴대전화를 분실한 경우, 아래 복구 코드 중 하나를 사용해 계정에 접근할 수 있습니다. <strong>복구 코드는 안전하게 보관해 주십시오.</strong> 이 코드를 인쇄해 중요한 서류와 함께 보관하는 것도 좋습니다.
     webauthn: 보안 키
   user_mailer:
+    announcement_published:
+      description: "%{domain}의 관리자가 공지사항을 게시했습니다:"
+      subject: 서비스 공지사항
+      title: "%{domain} 서비스 공지사항"
     appeal_approved:
       action: 계정 설정
       explanation: "%{strike_date}에 일어난 중재결정에 대한 소명을 %{appeal_date}에 작성했으며 승낙되었습니다. 당신의 계정은 정상적인 상태로 돌아왔습니다."
@@ -1895,8 +1935,8 @@ ko:
     terms_of_service_changed:
       agreement: "%{domain}을 계속 사용하는 것으로 약관에 동의하는 것으로 간주합니다. 약관에 동의하지 않는 경우 계정을 삭제함으로써 언제든 동의를 철회할 수 있습니다."
       changelog: '이번 변경사항의 주요 내용입니다:'
-      description: "%{domain}의 이용 약관이 변경되었기 때문에 발송된 이메일입니다. 변경된 전체 약관을 확인하시길 권합니다:"
-      description_html: '%{domain}의 이용 약관이 변경되었기 때문에 발송된 이메일입니다. <a href="%{path}" target="_blank">변경된 전체 약관</a>을 확인하시길 권합니다.'
+      description: "%{domain}의 이용 약관이 변경되었기 때문에 발송된 이메일입니다. 이 변경사항은 %{date}부터 효력을 발휘합니다. 변경된 전체 약관을 확인하시길 권합니다:"
+      description_html: '%{domain}의 이용 약관이 변경되었기 때문에 발송된 이메일입니다. 이 변경사항은 <strong>%{date}</strong>부터 효력을 발휘합니다. <a href="%{path}" target="_blank">변경된 전체 약관</a>을 확인하시길 권합니다.'
       sign_off: "%{domain} 팀"
       subject: 변경된 이용 약관
       subtitle: "%{domain}의 이용 약관이 변경됩니다"
diff --git a/config/locales/lad.yml b/config/locales/lad.yml
index 939a8826b2..c1364fbb01 100644
--- a/config/locales/lad.yml
+++ b/config/locales/lad.yml
@@ -462,6 +462,18 @@ lad:
       new:
         title: Importa blokos de domeno
       no_file: Dinguna dosya tiene sido eskojida
+    fasp:
+      debug:
+        callbacks:
+          delete: Efasa
+      providers:
+        name: Nombre
+        registrations:
+          confirm: Konfirma
+          reject: Refuza
+        save: Guadra
+        sign_in: Konektate
+        status: Estado
     follow_recommendations:
       description_html: "<strong>Las rekomendasyones de kuentos ayudan a los muevos utilizadores a topar presto kontenido enteresante</strong>. Kuando un utilizador no tiene enteraktuado kon otros lo sufisiente komo para djenerar rekomendasyones personalizadas de kuentos a las ke segir, en sus lugar se le rekomiendan estes kuentos. Se rekalkulan diariamente a partir de una mikstura de kuentos kon el mayor numero de enteraksyones rezientes i kon el mayor numero de suivantes lokales kon una lingua determinada."
       language: Para la lingua
@@ -812,6 +824,7 @@ lad:
       batch:
         remove_from_report: Kita del raporto
         report: Raporto
+      contents: Kontenidos
       deleted: Efasado
       favourites: Favoritos
       history: Estoria de versiones
@@ -901,6 +914,9 @@ lad:
       search: Bushka
       title: Etiketas
       updated_msg: Konfigurasyon de etiketas aktualizada kon sukseso
+    terms_of_service:
+      live: En bivo
+      publish: Publika
     title: Administrasyon
     trends:
       allow: Permete
@@ -1129,6 +1145,7 @@ lad:
       title: Konektate kon %{domain}
     sign_up:
       manual_review: Las enrejistrasyones en %{domain} pasan por la revizyon manuala de muestros moderadores. Para ayudarmos a prosesar tu enrejistrasyon, eskrive un poko sovre ti i por ke keres un kuento en %{domain}.
+      preamble: Kon un kuento en este sirvidor de Mastodon, podras segir a kualkier otra persona en el fediverso, endependientemente del sirvidor en el ke se tope.
       title: Kriya kuento de Mastodon en %{domain}.
     status:
       account_status: Estado del kuento
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 7659cfcbfb..f0da6d5f15 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -4,7 +4,7 @@ lt:
     about_mastodon_html: 'Ateities socialinis tinklas: jokių reklamų, jokių įmonių sekimo, etiškas dizainas ir decentralizacija! Turėk savo duomenis su Mastodon.'
     contact_missing: Nenustatyta
     contact_unavailable: Nėra
-    hosted_on: Mastodon talpinamas %{domain}
+    hosted_on: "„Mastodon“ talpinamas domene %{domain}"
     title: Apie
   accounts:
     followers:
@@ -294,6 +294,7 @@ lt:
       title: Audito žurnalas
       unavailable_instance: "(domeno pavadinimas neprieinamas)"
     announcements:
+      back: Atgal į skelbimus
       destroyed_msg: Skelbimas sėkmingai ištrintas.
       edit:
         title: Redaguoti skelbimą
@@ -302,6 +303,9 @@ lt:
       new:
         create: Sukurti skelbimą
         title: Naujas skelbimas
+      preview:
+        explanation_html: 'El. laiškas bus išsiųstas <strong>%{display_count} naudotojams</strong>. Į el. laišką bus įtrauktas toliau nurodytas tekstas:'
+        title: Peržiūrėti skelbimo pranešimą
       publish: Skelbti
       published_msg: Skelbimas sėkmingai paskelbtas.
       scheduled_for: Suplanuota %{time}
@@ -353,7 +357,7 @@ lt:
       opened_reports: atidaryti ataskaitos
       pending_appeals_html:
         few: "<strong>%{count}</strong> laukiantys apeliacijos"
-        many: "<strong>%{count}</strong> laukiama apeliacija"
+        many: "<strong>%{count}</strong> laukiamos apeliacijos"
         one: "<strong>%{count}</strong> laukiama apeliacija"
         other: "<strong>%{count}</strong> laukiančių apeliacijų"
       pending_reports_html:
@@ -1219,6 +1223,10 @@ lt:
     recovery_instructions_html: Jeigu prarandate prieiga prie telefono, jūs galite naudoti atkūrimo kodus esančius žemiau, kad atgautumėte priega prie savo paskyros.<strong>Laikykite atkūrimo kodus saugiai</strong> Pavyzdžiui, galite norėti juos išspausdinti, ir laikyti kartu su kitais svarbiais dokumentais.
     webauthn: Saugumo raktai
   user_mailer:
+    announcement_published:
+      description: "%{domain} administratoriai daro skelbimą:"
+      subject: Paslaugos skelbimas
+      title: "%{domain} paslaugos skelbimas"
     appeal_approved:
       action: Paskyros nustatymai
       subtitle: Tavo paskyros būklė vėl yra gera.
@@ -1240,8 +1248,6 @@ lt:
     terms_of_service_changed:
       agreement: Tęsiant naudojimąsi %{domain}, jūs sutinkate su šiomis sąlygomis. Jei nesutinkate su atnaujintomis sąlygomis, bet kuriuo metu galite nutraukti sutartį su %{domain} ištrindami savo paskyrą.
       changelog: Trumpai apie tai, ką šis naujinimas reiškia jums
-      description: 'Šį el. laišką gaunate, nes mes keičiame savo paslaugų sąlygas serveryje %{domain}. Kviečiame susipažinti su visomis atnaujintomis sąlygomis čia:'
-      description_html: Šį el. laišką gaunate, nes mes keičiame savo paslaugų sąlygas serveryje %{domain}. Kviečiame susipažinti su <a href="%{path}" target="_blank">visomis atnaujintomis sąlygomis čia</a>.
       sign_off: "%{domain} komanda"
       subject: Paslaugų sąlygų atnaujinimai
       subtitle: Keičiasi %{domain} paslaugų sąlygos
diff --git a/config/locales/lv.yml b/config/locales/lv.yml
index 2d256b475e..2bb5abf2de 100644
--- a/config/locales/lv.yml
+++ b/config/locales/lv.yml
@@ -19,10 +19,10 @@ lv:
     pin_errors:
       following: Tev ir jāseko personai, kuru vēlies atbalstīt
     posts:
-      one: Ziņa
-      other: Ziņas
-      zero: Ziņu
-    posts_tab_heading: Ziņas
+      one: Ieraksts
+      other: Ieraksti
+      zero: Ierakstu
+    posts_tab_heading: Ieraksti
     self_follow_error: Nav ļauts sekot savam kontam
   admin:
     account_actions:
@@ -32,24 +32,24 @@ lv:
       title: Veikt satura pārraudzības darbību %{acct}
     account_moderation_notes:
       create: Atstāt piezīmi
-      created_msg: Satura pārraudzības piezīme ir veiksmīgi izveidota.
-      destroyed_msg: Satura pārraudzības piezīme ir veiksmīgi iznīcināta.
+      created_msg: Satura pārraudzības piezīme ir sekmīgi izveidota.
+      destroyed_msg: Satura pārraudzības piezīme ir sekmīgi iznīcināta.
     accounts:
       add_email_domain_block: Liegt e-pasta domēnu
       approve: Apstiprināt
-      approved_msg: Veiksmīgi apstiprināts %{username} reģistrēšanās pieteikums
+      approved_msg: "%{username} reģistrēšanās pieteikums sekmīgi apstiprināts"
       are_you_sure: Vai esi pārliecināts?
       avatar: Profila attēls
       by_domain: Domēns
       change_email:
-        changed_msg: E-pasts veiksmīgi nomainīts!
+        changed_msg: E-pasta adrese sekmīgi nomainīta.
         current_email: Pašreizējais e-pasts
         label: Mainīt e-pastu
         new_email: Jaunā e-pasta adrese
         submit: Mainīt e-pastu
         title: Mainīt e-pastu %{username}
       change_role:
-        changed_msg: Loma veiksmīgi nomainīta!
+        changed_msg: Loma sekmīgi nomainīta.
         edit_roles: Pārvaldīt lietotāju lomas
         label: Mainīt lomu
         no_role: Nav lomas
@@ -74,7 +74,7 @@ lv:
       enable: Atsaldēt
       enable_sign_in_token_auth: Iespējot autentificēšanos ar e-pasta pilnvaru
       enabled: Iespējots
-      enabled_msg: Veiksmīgi atsaldēts %{username} konts
+      enabled_msg: "%{username} konts sekmīgi atsaldēts"
       followers: Sekotāji
       follows: Seko
       header: Galvene
@@ -92,7 +92,7 @@ lv:
       media_attachments: Multivides pielikumi
       memorialize: Pārvērst atmiņās
       memorialized: Piemiņa saglabāta
-      memorialized_msg: "%{username} veiksmīgi pārvērsts par piemiņas kontu"
+      memorialized_msg: "%{username} sekmīgi pārveidots par piemiņas kontu"
       moderation:
         active: Aktīvie
         all: Visi
@@ -120,19 +120,19 @@ lv:
       public: Publisks
       push_subscription_expires: PuSH abonements beidzas
       redownload: Atsvaidzināt profilu
-      redownloaded_msg: Veiksmīgi atsvaidzināts %{username} profils no izcelsmes
+      redownloaded_msg: "%{username} profils sekmīgi atsvaidzināts no pirmavota"
       reject: Noraidīt
-      rejected_msg: Veiksmīgi noraidīts %{username} reģistrēšanās pieteikums
+      rejected_msg: "%{username} reģistrēšanās pieteikums sekmīgi noraidīts"
       remote_suspension_irreversible: Šī konta dati ir neatgriezeniski dzēsti.
       remote_suspension_reversible_hint_html: Konts ir apturēts viņu serverī, un dati tiks pilnībā noņemti %{date}. Līdz tam attālais serveris var atjaunot šo kontu bez jebkādām negatīvām sekām. Ja vēlaties nekavējoties noņemt visus konta datus, varat to izdarīt tālāk.
       remove_avatar: Noņemt profila attēlu
       remove_header: Noņemt galveni
-      removed_avatar_msg: Veiksmīgi noņemts %{username} profila attēls
-      removed_header_msg: Veiksmīgi noņemts %{username} galvenes attēls
+      removed_avatar_msg: "%{username} profila attēls sekmīgi noņemts"
+      removed_header_msg: "%{username} galvenes attēls sekmīgi noņemts"
       resend_confirmation:
         already_confirmed: Šis lietotājs jau ir apstiprināts
         send: Atkārtoti nosūtīt apstiprinājuma saiti
-        success: Apstiprinājuma saite veiksmīgi nosūtīta!
+        success: Apstiprinājuma saite sekmīgi nosūtīta.
       reset: Atiestatīt
       reset_password: Atiestatīt paroli
       resubscribe: Pieteikties vēlreiz
@@ -144,15 +144,15 @@ lv:
       security_measures:
         only_password: Tikai parole
         password_and_2fa: Parole un 2FA
-      sensitive: Sensitīvs
-      sensitized: Atzīmēts kā sensitīvs
+      sensitive: Uzspiest atzīmēšanu kā jūtīgu
+      sensitized: Atzīmēts kā jūtīgs
       shared_inbox_url: Koplietotās iesūtnes URL
       show:
         created_reports: Sastādītie ziņojumi
         targeted_reports: Ziņojuši citi
       silence: Ierobežot
       silenced: Ierobežots
-      statuses: Ziņas
+      statuses: Ieraksti
       strikes: Iepriekšējie streiki
       subscribe: Abonēt
       suspend: Apturēt
@@ -161,14 +161,14 @@ lv:
       suspension_reversible_hint_html: Konta darbība ir apturēta, un dati tiks pilnībā noņemti %{date}. Līdz tam kontu var atjaunot bez jebkādām nelabvēlīgām sekām. Ja vēlies nekavējoties noņemt visus konta datus, to vari izdarīt zemāk.
       title: Konti
       unblock_email: Atbloķēt e-pasta adresi
-      unblocked_email_msg: Veiksmīgi atbloķēta %{username} e-pasta adrese
+      unblocked_email_msg: "%{username} e-pasta adreses liegšana sekmīgi atcelta"
       unconfirmed_email: Neapstiprināts e-pasts
-      undo_sensitized: Atcelt sensitivizēšanu
+      undo_sensitized: Atcelt uzspiestu atzīmēšanu kā jūtīgu
       undo_silenced: Atsaukt ierobežojumu
       undo_suspension: Atsaukt apturēšanu
-      unsilenced_msg: Veiksmīgi atsaukts %{username} konta ierobežojums
+      unsilenced_msg: "%{username} konta ierobežojums sekmīgi atsaukts"
       unsubscribe: Anulēt abonementu
-      unsuspended_msg: Veiksmīgi neapturēts %{username} konts
+      unsuspended_msg: "%{username} konts apturēšana sekmīgi atcelta"
       username: Lietotājvārds
       view_domain: Skatīt domēna kopsavilkumu
       warn: Brīdināt
@@ -176,7 +176,7 @@ lv:
       whitelisted: Atļauts federācijai
     action_logs:
       action_types:
-        approve_appeal: Apstiprināt Apelāciju
+        approve_appeal: Apstiprināt pārsūdzību
         approve_user: Apstiprināt lietotāju
         assigned_to_self_report: Piešķirt Pārskatu
         change_email_user: Mainīt lietotāja e-pasta adresi
@@ -218,19 +218,19 @@ lv:
         memorialize_account: Saglabāt Kontu Piemiņai
         promote_user: Izceltt Lietotāju
         publish_terms_of_service: Publicēt pakalpojuma izmantošanas noteikumus
-        reject_appeal: Noraidīt Apelāciju
+        reject_appeal: Noraidīt pārsūdzību
         reject_user: Noraidīt lietotāju
         remove_avatar_user: Noņemt profila attēlu
         reopen_report: Atkārtoti Atvērt Ziņojumu
-        resend_user: Atkārtoti nosūtīt Apstiprinājuma Pastu
+        resend_user: Atkārtoti nosūtīt apstiprinājuma e-pasta ziņojumu
         reset_password_user: Atiestatīt Paroli
         resolve_report: Atrisināt Ziņojumu
-        sensitive_account: Piespiedu sensitīvizēt kontu
+        sensitive_account: Uzspiesti atzimēt kontu kā jūtīgu
         silence_account: Ierobežot Kontu
         suspend_account: Apturēt Kontu
         unassigned_report: Atcelt Pārskata Piešķiršanu
         unblock_email_account: Atbloķēt e-pasta adresi
-        unsensitive_account: Atsaukt Konta Piespiedu Sensitivizēšanu
+        unsensitive_account: Atsaukt uzspiestu konta atzīmēšanu kā jūtīgu
         unsilence_account: Atcelt Konta Ierobežošanu
         unsuspend_account: Atcelt konta apturēšanu
         update_announcement: Atjaunināt Paziņojumu
@@ -241,7 +241,7 @@ lv:
         update_status: Atjaunināt ziņu
         update_user_role: Atjaunināt lomu
       actions:
-        approve_appeal_html: "%{name} apstiprināja satura pārraudzības lēmuma iebildumu no %{target}"
+        approve_appeal_html: "%{name} apstiprināja satura pārraudzības lēmuma pārsūdzību no %{target}"
         approve_user_html: "%{name} apstiprināja reģistrēšanos no %{target}"
         assigned_to_self_report_html: "%{name} piešķīra pārskatu %{target} sev"
         change_email_user_html: "%{name} nomainīja lietotāja %{target} e-pasta adresi"
@@ -259,9 +259,11 @@ lv:
         create_user_role_html: "%{name} nomainīja %{target} lomu"
         demote_user_html: "%{name} pazemināja lietotāju %{target}"
         destroy_announcement_html: "%{name} izdzēsa paziņojumu %{target}"
+        destroy_canonical_email_block_html: "%{name} atcēla e-pasta adreses liegumu ar jaucējvērtību %{target}"
         destroy_custom_emoji_html: "%{name} izdzēsa emocijzīmi %{target}"
         destroy_domain_allow_html: "%{name} neatļāva federāciju ar domēnu %{target}"
         destroy_domain_block_html: "%{name} atbloķēja domēnu %{target}"
+        destroy_email_domain_block_html: "%{name} atcēla e-pasta domēna %{target} liegumu"
         destroy_instance_html: "%{name} attīrija domēnu %{target}"
         destroy_ip_block_html: "%{name} izdzēsa nosacījumu priekš IP %{target}"
         destroy_status_html: "%{name} noņēma ziņu %{target}"
@@ -275,19 +277,19 @@ lv:
         memorialize_account_html: "%{name} pārvērta %{target} kontu par atmiņas lapu"
         promote_user_html: "%{name} paaugstināja lietotāju %{target}"
         publish_terms_of_service_html: "%{name} padarīja pieejamus pakalpojuma izmantošanas noteikumu atjauninājumus"
-        reject_appeal_html: "%{name} noraidīja satura pārraudzības lēmuma iebildumu no %{target}"
+        reject_appeal_html: "%{name} noraidīja satura pārraudzības lēmuma pārsūdzību no %{target}"
         reject_user_html: "%{name} noraidīja reģistrēšanos no %{target}"
         remove_avatar_user_html: "%{name} noņēma %{target} profila attēlu"
         reopen_report_html: "%{name} atkārtoti atvēra ziņojumu %{target}"
         resend_user_html: "%{name} atkārtoti nosūtīja %{target} apstiprinājuma e-pasta ziņojumu"
         reset_password_user_html: "%{name} atiestatīja paroli lietotājam %{target}"
         resolve_report_html: "%{name} atrisināja ziņojumu %{target}"
-        sensitive_account_html: "%{name} atzīmēja %{target} multividi kā sensitīvu"
+        sensitive_account_html: "%{name} atzīmēja %{target} informācijas nesēju kā jūtīgu"
         silence_account_html: "%{name} ierobežoja %{target} kontu"
         suspend_account_html: "%{name} apturēja %{target} kontu"
         unassigned_report_html: "%{name} nepiešķīra ziņojumu %{target}"
         unblock_email_account_html: "%{name} atbloķēja %{target} e-pasta adresi"
-        unsensitive_account_html: "%{name} atmarķēja %{target} multividi kā sensitīvu"
+        unsensitive_account_html: "%{name} atcēla %{target} informācijas nesēja atzīmēšanu kā jūtīgu"
         unsilence_account_html: "%{name} atcēla ierobežojumu %{target} kontam"
         unsuspend_account_html: "%{name} neapturēja %{target} kontu"
         update_announcement_html: "%{name} atjaunināja paziņojumu %{target}"
@@ -304,7 +306,8 @@ lv:
       title: Audita žurnāls
       unavailable_instance: "(domēna vārds nav pieejams)"
     announcements:
-      destroyed_msg: Paziņojums ir veiksmīgi izdzēsts!
+      back: Atgriezties pie paziņojumiem
+      destroyed_msg: Paziņojums sekmīgi izdzēsts.
       edit:
         title: Labot paziņojumu
       empty: Neviens paziņojums netika atrasts.
@@ -312,32 +315,35 @@ lv:
       new:
         create: Izveidot paziņojumu
         title: Jauns paziņojums
+      preview:
+        explanation_html: 'E-pasta ziņojums tiks nosūtīts <strong>%{display_count} lietotājiem</strong>. Šis teksts tiks iekļauts e-pasta ziņojumā:'
+        title: Priekšskatīt paziņojumu
       publish: Publicēt
-      published_msg: Paziņojums ir veiksmīgi publicēts!
+      published_msg: Paziņojums sekmīgi publicēts.
       scheduled_for: Plānots uz %{time}
       scheduled_msg: Paziņojums ieplānots publicēšanai!
       title: Paziņojumi
       unpublish: Atcelt publicēšanu
-      unpublished_msg: Paziņojuma publicēšana veiksmīgi atcelta!
-      updated_msg: Paziņojums ir veiksmīgi atjaunināts!
+      unpublished_msg: Paziņojuma publicēšana sekmīgi atcelta!
+      updated_msg: Paziņojums sekmīgi atjaunināts!
     critical_update_pending: Gaida kritisko atjauninājumu
     custom_emojis:
       assign_category: Piešķirt kategoriju
       by_domain: Domēns
-      copied_msg: Emocijas vietējā kopija ir veiksmīgi izveidota
+      copied_msg: Emocijzīmes vietējā kopija ir sekmīgi izveidota
       copy: Kopēt
       copy_failed_msg: Nevarēja izveidot šīs emocijzīmes vietējo kopiju
       create_new_category: Izveidot jaunu kategoriju
-      created_msg: Emocijzīme veiksmīgi izveidota!
+      created_msg: Emocijzīme sekmīgi izveidota.
       delete: Dzēst
-      destroyed_msg: Emocijzīme ir veiksmīgi iznīcināta!
+      destroyed_msg: Emocijzīme sekmīgi iznīcināta!
       disable: Atspējot
       disabled: Atspējots
-      disabled_msg: Šī emocijzīme ir veiksmīgi atspējota
+      disabled_msg: Šī emocijzīme sekmīgi atspējota
       emoji: Emocijzīmes
       enable: Iespējot
       enabled: Iespējots
-      enabled_msg: Šī emocijzīme ir veiksmīgi iespējota
+      enabled_msg: Šī emocijzīme ir sekmīgi iespējota
       image_hint: PNG vai GIF līdz %{size}
       list: Saraksts
       listed: Uzrakstītas
@@ -353,7 +359,7 @@ lv:
       unlist: Izslēgt
       unlisted: Neminētie
       update_failed_msg: Nevarēja atjaunināt šo emocijzīmi
-      updated_msg: Emocijzīme veiksmīgi atjaunināta!
+      updated_msg: Emocijzīme sekmīgi atjaunināta.
       upload: Augšupielādēt
     dashboard:
       active_users: aktīvie lietotāji
@@ -391,7 +397,7 @@ lv:
         title: Apelācijas
     domain_allows:
       add_new: Atļaut federāciju ar domēnu
-      created_msg: Domēns ir veiksmīgi atļauts federācijai
+      created_msg: Domēns tika sekmīgi atļauts federācijai
       destroyed_msg: Domēns ir aizliegts federācijai
       export: Eksportēt
       import: Importēt
@@ -404,7 +410,7 @@ lv:
         permanent_action: Apturēšanas atsaukšana neatjaunos nekādus datus vai attiecības.
         preamble_html: Tu gatavojies apturēt domēna <strong>%{domain}</strong> un tā apakšdomēnu darbību.
         remove_all_data: Tādējādi no tava servera tiks noņemts viss šī domēna kontu saturs, multivide un profila dati.
-        stop_communication: Jūsu serveris pārtrauks sazināties ar šiem serveriem.
+        stop_communication: Tavs serveris pārtrauks sazināties ar šiem serveriem.
         title: Apstiprināt domēna %{domain} bloķēšanu
         undo_relationships: Tādējādi tiks atsauktas jebkuras sekošanas attiecības starp šo un tavu serveru kontiem.
       created_msg: Domēna bloķēšana tagad tiek apstrādāta
@@ -429,9 +435,9 @@ lv:
       obfuscate: Apslēpt domēna vārdu
       obfuscate_hint: Daļēji apslēpt domēna nosaukumu sarakstā, ja ir iespējota domēna ierobežojumu saraksta reklamēšana
       private_comment: Privāts komentārs
-      private_comment_hint: Atstāj komentāru par šo domēna ierobežojumu moderatoru iekšējai lietošanai.
+      private_comment_hint: Atstāt piebildi par šo domēna ierobežojumu satura pārraudzītāju iekšējai lietošanai.
       public_comment: Publisks komentārs
-      public_comment_hint: Atstāj komentāru par šo domēna ierobežojumu plašai sabiedrībai, ja ir iespējota domēnu ierobežojumu saraksta reklamēšana.
+      public_comment_hint: Piebilde par šo domēna ierobežojumu vispārējai sabiedrībai, ja ir iespējota domēnu ierobežojumu saraksta reklamēšana.
       reject_media: Noraidīt multivides failus
       reject_media_hint: Noņem lokāli saglabātos multivides failus un atsaka tos lejupielādēt nākotnē. Nav nozīmes apturēšanai
       reject_reports: Noraidīt ziņojumus
@@ -445,6 +451,7 @@ lv:
         one: "%{count} mēģinājums pagājušajā nedēļā"
         other: "%{count} reģistrēšanās mēģinājumi pagājušajā nedēļā"
         zero: "%{count} mēģinājumu pagājušajā nedēļā"
+      created_msg: Sekmīgi liegts e-pasta domēns
       delete: Dzēst
       dns:
         types:
@@ -453,7 +460,10 @@ lv:
       new:
         create: Pievienot domēnu
         resolve: Atrisināt domēnu
+        title: Liegt jaunu e-pasta domēnu
+      no_email_domain_block_selected: Neviens e-pasta domēna bloks netika mainīts, jo neviens netika atlasīts
       not_permitted: Nav atļauta
+      resolved_dns_records_hint_html: Domēna vārds saistās ar zemāk norādītajiem MX domēniem, kuri beigās ir atbildīgi par e-pasta pieņemšana. MX domēna liegšana liegs reģistrēšanos no jebkuras e-pasta adreses, kas izmanto to pašu MX domēnu, pat ja redzamais domēna vārds ir atšķirīgs. <strong>Jāuzmanās, lai neliegtu galvenos e-pasta pakalpojuma sniedzējus.</strong>
       resolved_through_html: Atrisināts, izmantojot %{domain}
       title: Bloķētie e-pasta domēni
     export_domain_allows:
@@ -471,6 +481,35 @@ lv:
       new:
         title: Importēt bloķētos domēnus
       no_file: Nav atlasīts neviens fails
+    fasp:
+      debug:
+        callbacks:
+          created_at: Izveidots
+          delete: Izdzēst
+          ip: IP adrese
+          request_body: Pieprasījuma saturs
+          title: Atkļūdošanas atpakaļsaukumi
+      providers:
+        base_url: Pamata URL
+        callback: Atpakaļsaukums
+        delete: Izdzēst
+        edit: Labot nodrošinātāju
+        finish_registration: Pabeigt reģistrēšanos
+        name: Nosaukums
+        providers: Nodrošinātāji
+        public_key_fingerprint: Publiskās atslēgas pirkstu nospiedums
+        registration_requested: Pieprasīta reģistrēšanās
+        registrations:
+          confirm: Apstiprināt
+          description: Tu saņēmi reģistrēšanos no FĀPS. Tā ir jānoraida, ja to neveici. Ja veici, rūpīgi jāsalīdzina nosaukums un atslēgas pirkstu nospiedums, pirms reģistrēšanās apstiprināšanas.
+          reject: Noraidīt
+          title: Apstiprināt FĀPS reģistrēšanos
+        save: Saglabāt
+        select_capabilities: Atlasīt spējas
+        sign_in: Pieteikties
+        status: Stāvoklis
+        title: Fediverse ārējie pakalpojumu sniedzēji
+      title: FĀPS
     follow_recommendations:
       description_html: "<strong>Sekošanas ieteikumi palīdz jauniem lietotājiem ātri arast saistošu saturu</strong>. Kad lietotājs nav pietiekami mijiedarbojies ar citiem, lai veidotos pielāgoti sekošanas iteikumi, tiek ieteikti šie konti. Tie tiek pārskaitļoti ik dienas, izmantojot kontu, kuriem ir augstākās nesenās iesaistīšanās un lielākais vietējo sekotāju skaits norādītajā valodā."
       language: Valodai
@@ -495,7 +534,7 @@ lv:
           zero: Neizdevušies mēģinājumi %{count} dienās.
         no_failures_recorded: Nav reģistrētu kļūdu.
         title: Pieejamība
-        warning: Pēdējais mēģinājums izveidot savienojumu ar šo serveri ir bijis neveiksmīgs
+        warning: Pēdējais mēģinājums izveidot savienojumu ar šo serveri ir bijis nesekmīgs
       back_to_all: Visas
       back_to_limited: Ierobežotās
       back_to_warning: Brīdinājums
@@ -521,7 +560,7 @@ lv:
         instance_languages_dimension: Populārākās valodas
         instance_media_attachments_measure: saglabātie multivides pielikumi
         instance_reports_measure: ziņojumi par viņiem
-        instance_statuses_measure: saglabātās ziņas
+        instance_statuses_measure: saglabātie ieraksti
       delivery:
         all: Visas
         clear: Notīrīt piegādes kļūdas
@@ -564,7 +603,7 @@ lv:
       title: Uzaicinājumi
     ip_blocks:
       add_new: Izveidot noteikumu
-      created_msg: Veiksmīgi pievienots jauns IP noteikums
+      created_msg: Sekmīgi pievienota jauna IP kārtula
       delete: Dzēst
       expires_in:
         '1209600': 2 nedēļas
@@ -586,7 +625,7 @@ lv:
       disable: Atspējot
       disabled: Atspējots
       enable: Iespējot
-      enable_hint: Kad tas būs iespējots, tavs serveris abonēs visas publiskās ziņas no šī releja un sāks tam sūtīt šī servera publiskās ziņas.
+      enable_hint: Tiklīdz iespējots, serveris abonēs visus šī releja publiskos ierakstus un sāks tam sūtīt šī iservera publiskos ierakstus.
       enabled: Iespējots
       inbox_url: Releja URL
       pending: Gaida apstiprinājumu no releja
@@ -596,8 +635,8 @@ lv:
       status: Statuss
       title: Releji
     report_notes:
-      created_msg: Ziņojuma piezīme ir veiksmīgi izveidota!
-      destroyed_msg: Ziņojuma piezīme ir veiksmīgi izdzēsta!
+      created_msg: Ziņojuma piezīme sekmīgi izveidota.
+      destroyed_msg: Ziņojuma piezīme sekmīgi izdzēsta.
     reports:
       account:
         notes:
@@ -608,7 +647,7 @@ lv:
       action_taken_by: Veiktā darbība
       actions:
         delete_description_html: Raksti, par kurām ziņots, tiks dzēsti, un tiks reģistrēts brīdinājums, lai palīdzētu tev izvērst turpmākos pārkāpumus saistībā ar to pašu kontu.
-        mark_as_sensitive_description_html: Multividesu faili ziņojumos, par kuriem ziņots, tiks atzīmēti kā sensitīvi, un tiks reģistrēts brīdinājums, lai palīdzētu tev izvērst turpmākus pārkāpumus saistībā ar to pašu kontu.
+        mark_as_sensitive_description_html: Informācijas nesēji ierakstos, par kuriem ziņots, tiks atzīmēti kā jūtīgi, un tiks iegrāmatots brīdinājums, lai palīdzētu ziņot par turpmākiem tā paša konta pārkāpumiem.
         other_description_html: Skatīt vairāk iespēju kontrolēt konta uzvedību un pielāgot saziņu ar paziņoto kontu.
         resolve_description_html: Pret norādīto kontu netiks veiktas nekādas darbības, netiks reģistrēts brīdinājums, un ziņojums tiks slēgts.
         silence_description_html: Konts būs redzams tikai tiem, kas tam jau seko vai meklē to manuāli, ievērojami ierobežojot tā sasniedzamību. To vienmēr var atgriezt. Tiek aizvērti visi šī konta pārskati.
@@ -617,9 +656,12 @@ lv:
       actions_description_remote_html: Izlem, kādas darbības jāveic, lai atrisinātu šo ziņojumu. Tas ietekmēs tikai to, kā <strong>tavs</strong> serveris sazinās ar šo attālo kontu un apstrādā tā saturu.
       actions_no_posts: Šim ziņojumam nav saistītu ierakstu, kurus izdzēst
       add_to_report: Pievienot varāk paziņošanai
+      already_suspended_badges:
+        local: Jau ir apturēts šajā serverī
+        remote: Jau ir apturēts viņu serverī
       are_you_sure: Vai esi pārliecināts?
       assign_to_self: Piešķirt man
-      assigned: Piešķirtais moderators
+      assigned: Piešķirtais satura pārraudzītājs
       by_target_domain: Ziņotā konta domēns
       cancel: Atcelt
       category: Kategorija
@@ -635,7 +677,7 @@ lv:
       forwarded_replies_explanation: Šis ziņojums ir no attāla lietotāja un par attālu saturu. Tas tika pārvirzīts šeit, jo saturs, par kuru tika ziņots, ir atbilde vienam no šī servera lietotājiem.
       forwarded_to: Pārsūtīti %{domain}
       mark_as_resolved: Atzīmēt kā atrisinātu
-      mark_as_sensitive: Atzīmēt kā sensitīvu
+      mark_as_sensitive: Atzīmēt kā jūtīgu
       mark_as_unresolved: Atzīmēt kā neatrisinātu
       no_one_assigned: Neviena
       notes:
@@ -645,8 +687,8 @@ lv:
         delete: Dzēst
         placeholder: Jāapraksta veiktās darbības vai jebkuri citi saistītie atjauninājumi...
         title: Piezīmes
-      notes_description_html: Skati un atstāj piezīmes citiem moderatoriem un sev nākotnei
-      processed_msg: 'Pārskats #%{id} veiksmīgi apstrādāts'
+      notes_description_html: Apskatīt un atstāt piezīmes citiem satura pārraudzītājiem un sev nākotnei
+      processed_msg: 'Ziņojums #%{id} sekmīgi apstrādāts'
       quick_actions_description_html: 'Veic ātro darbību vai ritini uz leju, lai skatītu saturu, par kuru ziņots:'
       remote_user_placeholder: attālais lietotājs no %{instance}
       reopen: Atkārtoti atvērt ziņojumu
@@ -655,7 +697,7 @@ lv:
       reported_by: Ziņoja
       reported_with_application: Ziņots no lietotnes
       resolved: Atrisināts
-      resolved_msg: Ziņojums veiksmīgi atrisināts!
+      resolved_msg: Ziņojums sekmīgi atrisināts.
       skip_to_actions: Pāriet uz darbībām
       status: Statuss
       statuses: Ziņotais saturs
@@ -663,12 +705,12 @@ lv:
       summary:
         action_preambles:
           delete_html: 'Jūs gatavojaties <strong>noņemt</strong> dažas no lietotāja <strong>@%{acct}</strong> ziņām. Tas:'
-          mark_as_sensitive_html: 'Jūs gatavojaties <strong>atzīmēt</strong> dažas no lietotāja <strong>@%{acct}</strong> ziņām kā <strong>sensitīvas</strong>. Tas:'
+          mark_as_sensitive_html: 'Tu gatavojies <strong>atzīmēt</strong> dažus no lietotāja <strong>@%{acct}</strong> ierakstiem kā <strong>jūtīgus</strong>. Tas:'
           silence_html: 'Jūs gatavojaties <strong>ierobežot</strong> <strong>@%{acct}</strong> kontu. Tas:'
           suspend_html: 'Jūs gatavojaties <strong>apturēt</strong> <strong>@%{acct}</strong> kontu. Tas:'
         actions:
-          delete_html: Noņemt aizskarošās ziņas
-          mark_as_sensitive_html: Atzīmēt aizskarošo ziņu multivides saturu kā sensitīvu
+          delete_html: Noņemt aizskarošos ierakstus
+          mark_as_sensitive_html: Atzīmēt aizskarošo ierakstu informācijas nesējus kā jūtīgus
           silence_html: Ievērojami ierobežo <strong>@%{acct}</strong> sasniedzamību, padarot viņa profilu un saturu redzamu tikai cilvēkiem, kas jau seko tam vai pašrocīgi uzmeklē profilu
           suspend_html: Apturēt <strong>@%{acct}</strong>, padarot viņu profilu un saturu nepieejamu un neiespējamu mijiedarbību ar
         close_report: 'Atzīmēt ziņojumu #%{id} kā atrisinātu'
@@ -676,8 +718,9 @@ lv:
         delete_data_html: Dzēsiet lietotāja <strong>@%{acct}</strong> profilu un saturu pēc 30 dienām, ja vien to darbība pa šo laiku netiks atcelta
         preview_preamble_html: "<strong>@%{acct}</strong> saņems brīdinājumu ar šādu saturu:"
         record_strike_html: Ierakstiet brīdinājumu pret <strong>@%{acct}</strong>, lai palīdzētu jums izvērst turpmākus pārkāpumus no šī konta
+        send_email_html: Nosūtīt <strong>@%{acct}</strong> brīdinājuma e-pasta ziņojumu
         warning_placeholder: Izvēles papildu pamatojums satura pārraudzības darbībai.
-      target_origin: Ziņotā konta izcelsme
+      target_origin: Konta, par kuru ziņots, izcelsme
       title: Ziņojumi
       unassign: Atsaukt
       unknown_action_msg: 'Nezināms konts: %{action}'
@@ -717,6 +760,7 @@ lv:
         manage_appeals: Pārvaldīt Pārsūdzības
         manage_appeals_description: Ļauj lietotājiem pārskatīt iebildumus pret satura pārraudzības darbībām
         manage_blocks: Pārvaldīt Bloķus
+        manage_blocks_description: Ļauj lietotājiem liegt e-pasta pakalpojumu sniedzējus un IP adreses
         manage_custom_emojis: Pārvaldīt Pielāgotās Emocijzīmes
         manage_custom_emojis_description: Ļauj lietotājiem pārvaldīt pielāgotās emocijzīmes serverī
         manage_federation: Pārvaldīt Federāciju
@@ -734,6 +778,7 @@ lv:
         manage_taxonomies: Pārvaldīt Taksonomijas
         manage_taxonomies_description: Ļauj lietotājiem pārskatīt aktuālāko saturu un atjaunināt tēmturu iestatījumus
         manage_user_access: Pārvaldīt Lietotāju Piekļuves
+        manage_user_access_description: Ļauj lietotājiem atspējot citu lietotāju divupakāpju autentificēšanos, mainīt savu e-pasta adresi un atiestatīt savu paroli
         manage_users: Pārvaldīt Lietotājus
         manage_users_description: Ļauj lietotājiem skatīt citu lietotāju informāciju un veikt pret viņiem satura pārraudzības darbības
         manage_webhooks: Pārvaldīt Tīmekļa Aizķeres
@@ -769,7 +814,7 @@ lv:
         title: Pieprasīt jaunajiem lietotājiem atrisināt CAPTCHA, lai apstiprinātu savu kontu
       content_retention:
         danger_zone: Bīstama sadaļa
-        preamble: Kontrolē, kā Mastodon tiek glabāts lietotāju ģenerēts saturs.
+        preamble: Pārraugi, kā Mastodon tiek glabāts lietotāju izveidots saturs!
         title: Satura saglabāšana
       default_noindex:
         desc_html: Ietekmē visus lietotājus, kuri paši nav mainījuši šo iestatījumu
@@ -805,9 +850,10 @@ lv:
       title: Servera iestatījumi
     site_uploads:
       delete: Dzēst augšupielādēto failu
-      destroyed_msg: Vietnes augšupielāde ir veiksmīgi izdzēsta!
+      destroyed_msg: Vietnes augšupielāde sekmīgi izdzēsta.
     software_updates:
       critical_update: Kritiski - lūdzu, ātri atjaunini
+      description: Ir ieteicams uzturēt savu Mastodon serveri atjauninātu, lai gūtu labumu no jaunākajiem labojumiem un iespējām. Vēl jo vairāk, dažreiz ir ļoti svarīgi savlaicīgi atjaunināt Mastodon, lai izvairītos no drošības nepilnībām. Šo iemeslu dēļ Mastodon pārbauda atjauninājumus ik pēc 30 minūtēm, un paziņos atbilstoši e-pasta paziņojumu iestatījumiem.
       documentation_link: Uzzināt vairāk
       release_notes: Laidiena piezīmes
       title: Pieejamie atjauninājumi
@@ -836,12 +882,12 @@ lv:
         title: Multivide
       metadata: Metadati
       no_history: Šis ieraksts nav bijis labots
-      no_status_selected: Neviena ziņa netika mainīta, jo neviena netika atlasīta
+      no_status_selected: Neviens ieraksts netika mainīts, jo nekas netika atlasīts
       open: Atvērt ziņu
-      original_status: Oriģinālā ziņa
+      original_status: Sākotnējais ieraksts
       reblogs: Reblogi
       replied_to_html: Atbildēja %{acct_link}
-      status_changed: Ziņa mainīta
+      status_changed: Ieraksts izmainīts
       status_title: Publicēja @%{name}
       title: Konta ieraksti - @%{name}
       trending: Aktuāli
@@ -852,9 +898,9 @@ lv:
       actions:
         delete_statuses: "%{name} izdzēsa %{target} publikācijas"
         disable: "%{name} iesaldēja %{target} kontu"
-        mark_statuses_as_sensitive: "%{name} atzīmēja %{target} ziņu kā sensitīvu"
+        mark_statuses_as_sensitive: "%{name} atzīmēja %{target} ierakstu kā jūtīgu"
         none: "%{name} nosūtīja brīdinājumu %{target}"
-        sensitive: "%{name} atzīmēja %{target} kontu kā sensitīvu"
+        sensitive: "%{name} atzīmēja %{target} kontu kā jūtīgu"
         silence: "%{name} ierobežoja %{target} kontu"
         suspend: "%{name} apturēja %{target} kontu"
       appeal_approved: Pārsūdzēts
@@ -862,7 +908,7 @@ lv:
       appeal_rejected: Apelācija noraidīta
     system_checks:
       database_schema_check:
-        message_html: Notiek datubāzu migrācijas. Lūdzu, palaid tās, lai nodrošinātu, ka lietojumprogramma darbojas, kā paredzēts
+        message_html: Ir nepabeigtas datubāzes migrācijas. Lūgums palaist tās, lai nodrošinātu, ka lietotne darbojas, kā paredzēts
       elasticsearch_health_red:
         message_html: Elasticsearch klasteris ir neveselīgs (sarkans statuss), meklēšanas līdzekļi nav pieejami
       elasticsearch_health_yellow:
@@ -904,11 +950,13 @@ lv:
         message_html: "<strong>Tava objektu krātuve ir nepareizi konfigurēta. Tavu lietotāju privātums ir apdraudēts.</strong>"
     tags:
       moderation:
+        not_trendable: Nav izplatīts
         not_usable: Nav izmantojams
         pending_review: Gaida pārskatīšanu
         review_requested: Pieprasīta pārskatīšana
         reviewed: Pārskatīts
         title: Stāvoklis
+        trendable: Izplatīts
         unreviewed: Nepārskatīts
         usable: Izmantojams
       name: Nosaukums
@@ -929,9 +977,10 @@ lv:
       generate: Izmantot sagatavi
       generates:
         action: Izveidot
-        chance_to_review_html: "<strong>Izveidotie pakalpojuma izmantošanas noteikumi netiks automātiski publicēti.</strong> Būs iespēja izskatīt iznākumu. Lūgums norādīt nepieciešamo informāciju, lai turpinātu."
+        chance_to_review_html: "<strong>Izveidotie pakalpojuma izmantošanas noteikumi netiks laisti klajā automātiski.</strong> Būs iespēja izskatīt iznākumu. Lūgums norādīt nepieciešamo informāciju, lai turpinātu."
         explanation_html: Pakalpojuma izmantošanas noteikumu sagatave tiek piedāvāta tikai izzināšanas nolūkam, un to nevajadzētu izmantot kā juridisku padomu jebkurā jautājumā. Lūgums sazināties ar savu juridisko padomdevēju par saviem apstākļiem un noteiktiem juridiskiem jautājumiem.
         title: Pakalpojuma izmantošānas noteikumu uzstādīšana
+      going_live_on_html: Darbībā, spēkā no %{date}
       history: Vēsture
       live: Darbībā
       no_history: Nav ierakstu par pakalpojuma izmantošanas noteikumu izmaiņām.
@@ -954,10 +1003,16 @@ lv:
     trends:
       allow: Atļaut
       approved: Apstiprināts
+      confirm_allow: Vai tiešām atļaut atlasītos tēmturus?
+      confirm_disallow: Vai tiešām neatļaut atlasītās birkas?
       disallow: Neatļaut
       links:
         allow: Atļaut saiti
         allow_provider: Atļaut publicētāju
+        confirm_allow: Vai tiešām atļaut atlasītās saites?
+        confirm_allow_provider: Vai tiešām atļaut atlasītos nodrošinātājus?
+        confirm_disallow: Vai tiešām neatļaut atlasītās saites?
+        confirm_disallow_provider: Vai tiešām neatļaut atlasītos nodrošinātājus?
         description_html: Šīs ir saites, kuras pašlaik bieži koplieto konti, no kuriem Tavs serveris redz ziņas. Tas var palīdzēt Taviem lietotājiem uzzināt, kas notiek pasaulē. Neviena saite netiek publiski rādīta, līdz tu apstiprini izdevēju. Tu vari arī atļaut vai noraidīt atsevišķas saites.
         disallow: Neatļaut saiti
         disallow_provider: Neatļaut publicētāju
@@ -982,6 +1037,10 @@ lv:
       statuses:
         allow: Ļaut veikt ierakstus
         allow_account: Atļaut autoru
+        confirm_allow: Vai tiešām atļaut atlasītos stāvokļus?
+        confirm_allow_account: Vai tiešām atļaut atlasītos kontus?
+        confirm_disallow: Vai tiešām neatļaut atlasītos stāvokļus?
+        confirm_disallow_account: Vai tiešām neatļaut atlasītos kontus?
         description_html: Šie ir ieraksti, par kuriem zina Tavs serveris un kuri pašlaik tiek daudz kopīgoti un pievienoti izlasēm. Tas var palīdzēt jaunajiem lietotājiem un tiem, kuri atgriežas, atrast vairāk cilvēku, kam sekot. Neviens ieraksts netiek publiski rādīts, līdz apstiprināsi autoru un ja autors ļauj savu kontu ieteikt citiem. Tu vari arī atļaut vai noraidīt atsevišķus ierakstus.
         disallow: Neļaut veikt ierakstus
         disallow_account: Neatļaut autoru
@@ -1027,7 +1086,7 @@ lv:
     webhooks:
       add_new: Pievienot galapunktu
       delete: Dzēst
-      description_html: Izmantojot <strong>tīmekļa aizķeri</strong>, Mastodon var nosūtīt jūsu lietojumprogrammai <strong>reāllaika paziņojumus</strong> par izvēlētajiem notikumiem, lai tava lietojumprogramma varētu <strong>automātiski izraisīt reakcijas</strong>.
+      description_html: "<strong>Tīmekļa aizķere</strong> ļauj Mastodon nosūtīt <strong>reāllaika paziņojumus</strong> Tavai lietotnei par izvēlētajiem notikumiem, lai Tava lietotne varētu <strong>automātiski izraisīt atbildes darbības</strong>."
       disable: Atspējot
       disabled: Atspējots
       edit: Labot galapunktu
@@ -1046,13 +1105,16 @@ lv:
       title: Tīmekļa āķi
       webhook: Tīmekļa āķis
   admin_mailer:
+    auto_close_registrations:
+      body: Nesenu satura pārraudzības darbību trūkuma dēļ reģistrācija %{instance} ir automātiski pārslēgta nepieciešamība pēc pašrocīgas izskatīšanas, lai novērstu %{instance} izmantošana kā platformu iespējami sliktiem dalībniekiem. Jebkurā brīdī var ieslēgt atpakaļ atvērtu reģistrēšanos.
+      subject: Reģistrēšanās %{instance} tika automātiski pārslēgta, lai pieprasītu apstiprināšanu
     new_appeal:
       actions:
         delete_statuses: lai izdzēstu viņu ierakstus
         disable: lai iesaldētu viņu kontu
-        mark_statuses_as_sensitive: lai atzīmētu viņu ziņas kā sensitīvas
+        mark_statuses_as_sensitive: lai atzīmētu viņu ierakstus kā jūtīgus
         none: brīdinājums
-        sensitive: lai atzīmētu viņu kontu kā sensitīvu
+        sensitive: lai atzīmētu viņu kontu kā jūtīgu
         silence: lai ierobežotu viņu kontu
         suspend: lai apturētu viņu kontu
       body: "%{target} iebilst %{action_taken_by} satura pārraudzības lēmumam no %{date}, kas bija %{type}. Viņi rakstīja:"
@@ -1082,8 +1144,8 @@ lv:
       subject: Tiek pārskatītas jaunas tendences %{instance}
   aliases:
     add_new: Izveidot aizstājvārdu
-    created_msg: Veiksmīgi izveidots jauns aizstājvārds. Tagad vari sākt pārvietošanu no vecā konta.
-    deleted_msg: Aizstājvārds tika veiksmīgi noņemts. Pāreja no šī konta uz šo vairs nebūs iespējama.
+    created_msg: Sekmīgi izveidots jauns aizstājvārds. Tagad vari uzsākt pārvietošanu no vecā konta.
+    deleted_msg: Aizstājvārds tika sekmīgi noņemts. Pāreja no tā konta uz šo vairs nebūs iespējama.
     empty: Tev nav aizstājvārdu.
     hint_html: Ja vēlies pāriet no cita konta uz šo, šeit vari izveidot aizstājvārdu, kas ir nepieciešams, lai varētu turpināt sekotāju pārvietošanu no vecā konta uz šo. Šī darbība pati par sevi ir <strong>nekaitīga un atgriezeniska</strong>. <strong>Konta migrācija tiek sākta no vecā konta</strong>.
     remove: Atsaistīt aizstājvārdu
@@ -1097,7 +1159,7 @@ lv:
       body: Mastodon ir tulkojuši brīvprātīgie.
       guide_link: https://crowdin.com/project/mastodon
       guide_link_text: Ikviens var piedalīties.
-    sensitive_content: Sensitīvs saturs
+    sensitive_content: Jūtīgs saturs
   application_mailer:
     notification_preferences: Mainīt e-pasta uztādījumus
     salutation: "%{name},"
@@ -1107,11 +1169,11 @@ lv:
     view_profile: Skatīt profilu
     view_status: Skatīt ziņu
   applications:
-    created: Lietojumprogramma ir veiksmīgi izveidota
-    destroyed: Lietojumprogramma ir veiksmīgi dzēsta
+    created: Lietotne sekmīgi izveidota
+    destroyed: Lietotnes sekmīgi izdzēsta
     logout: Iziet
-    regenerate_token: Atjaunot piekļuves marķieri
-    token_regenerated: Piekļuves marķieris veiksmīgi atjaunots
+    regenerate_token: Atkārtoti izveidot piekļuves pilnvaru
+    token_regenerated: Piekļuves pilnvara sekmīgi izveidota no jauna
     warning: Esi ļoti uzmanīgs ar šiem datiem. Nekad nedalies ne ar vienu ar tiem!
     your_token: Tavs piekļuves marķieris
   auth:
@@ -1140,7 +1202,7 @@ lv:
     dont_have_your_security_key: Vai tev nav drošības atslēgas?
     forgot_password: Aizmirsi paroli?
     invalid_reset_password_token: Paroles atiestatīšanas pilnvara nav derīga, vai tai ir beidzies derīgums. Lūdzu, pieprasi jaunu.
-    link_to_otp: Jāievada divpakāpju kods no tālruņa vai atkopšanas kods
+    link_to_otp: Jāievada divpakāpju kods no tālruņa vai atkpes kods
     link_to_webauth: Lieto savu drošības atslēgas iekārtu
     log_in_with: Pieslēgties ar
     login: Pieteikties
@@ -1164,14 +1226,15 @@ lv:
       accept: Pieņemt
       back: Atpakaļ
       invited_by: 'Tu vari pievienoties %{domain}, pateicoties uzaicinājumam, ko saņēmi no:'
-      preamble: Tos iestata un ievieš %{domain} moderatori.
-      preamble_invited: Pirms turpināt, lūdzu, apsver galvenos noteikumus, ko noteikuši %{domain} moderatori.
+      preamble: Tos iestata un ievieš %{domain} satura pārraudzītāji.
+      preamble_invited: Pirms turpināt, lūgums apsvērt pamatnoteikumus, kurus norādījuši %{domain} satura pārraudzītāji.
       title: Daži pamatnoteikumi.
       title_invited: Tu esi uzaicināts.
     security: Drošība
     set_new_password: Iestatīt jaunu paroli
     setup:
       email_below_hint_html: Jāpārbauda sava surogātpasta mape vai jāpieprasa vēl vienu! Savu e-pasta adresi var labot, ja tā ir nepareiza.
+      email_settings_hint_html: Jāatver saite, kuru mēs nosūtījām uz %{email}, lai sāktu izmantot Mastodon. Mēs gaidīsim šeit pat.
       link_not_received: Vai nesaņēmi sati?
       new_confirmation_instructions_sent: Pēc dažām minūtēm saņemsi jaunu e-pasta ziņojumu ar apstiprinājuma saiti.
       title: Pārbaudi savu iesūtni
@@ -1179,22 +1242,28 @@ lv:
       preamble_html: Jāpiesakās ar saviem <strong>%{domain}</strong> piekļuves datiem. Ja konts tiek mitināts citā serverī, šeit nevarēs pieteikties.
       title: Pieteikties %{domain}
     sign_up:
-      manual_review: Reģistrācijas domēnā %{domain} manuāli pārbauda mūsu moderatori. Lai palīdzētu mums apstrādāt tavu reģistrāciju, uzraksti mazliet par sevi un to, kāpēc vēlies kontu %{domain}.
+      manual_review: Reģistrāciju %{domain} pašrocīgi izskata mūsu satura pārraudzītāji. Lai palīdzētu mums apstrādāt Tavu reģistrāciju, uzraksti mazliet par sevi un to, kāpēc vēlies kontu %{domain}!
+      preamble: Ar kontu šajā Mastodon serverī varēsi sekot jebkuram citam cilvēkam fediversā, neatkarīgi no tā, kur tiek mitināts viņu konts.
       title: Atļauj tevi iestatīt %{domain}.
     status:
       account_status: Konta statuss
       confirming: Gaida e-pasta adreses apstiprināšanas pabeigšanu.
       functional: Tavs konts ir pilnā darba kārtībā.
+      pending: Tavs pieteikums ir rindā uz izskatīšanu, ko veic mūsu personāls. Tas var aizņemt kādu laiku. Tu saņemsi e-pasta ziņojumu, ja Tavs pieteikums tiks apstiprināts.
       redirecting_to: Tavs konts ir neaktīvs, jo pašlaik tas tiek novirzīts uz %{acct}.
       self_destruct: Tā kā %{domain} tiek slēgts, tu iegūsi tikai ierobežotu piekļuvi savam kontam.
       view_strikes: Skati iepriekšējos brīdinājumus par savu kontu
     too_fast: Veidlapa ir iesniegta pārāk ātri, mēģini vēlreiz.
     use_security_key: Lietot drošības atslēgu
     user_agreement_html: Es esmu izlasījis un piekrītu <a href="%{terms_of_service_path}" target="_blank">pakalpojuma izmantošanas noteikumiem</a> un <a href="%{privacy_policy_path}" target="_blank">privātuma nosacījumiem</a>
+    user_privacy_agreement_html: Es izlasīju un piekrītu <a href="%{privacy_policy_path}" target="_blank">privātuma pamatnostādnēm</a>
   author_attribution:
     example_title: Parauga teksts
+    hint_html: Vai Tu Raksti ziņu vai emuāra rakstus ārpus Mastodon? Pārraugi apliecinājumus, kad raksti tiek kopīgoti Mastodon.
+    instructions: 'Jāpārliecinās, ka šis kods ir raksta HTML:'
     more_from_html: Vairāk no %{name}
     s_blog: "%{name} emuāri"
+    then_instructions: Tad jāpievieno publicējuma domēna vārds zemāk esošajā laukā.
     title: Autora attiecinājums
   challenge:
     confirm: Turpināt
@@ -1227,7 +1296,7 @@ lv:
     confirm_password: Ievadi savu pašreizējo paroli, lai pārbaudītu savu identitāti
     confirm_username: Ievadi savu lietotājvārdu, lai apstiprinātu procedūru
     proceed: Dzēst kontu
-    success_msg: Tavs konts tika veiksmīgi dzēsts
+    success_msg: Tavs konts tika sekmīgi izdzēsts
     warning:
       before: 'Pirms turpināšanas lūgums uzmanīgi izlasīt šīs piezīmes:'
       caches: Citu serveru kešatmiņā saglabātais saturs var saglabāties
@@ -1243,10 +1312,10 @@ lv:
     strikes:
       action_taken: Veiktā darbība
       appeal: Apelācija
-      appeal_approved: Šis brīdinājums ir veiksmīgi pārsūdzēts un vairs nav spēkā
+      appeal_approved: Šis brīdinājums tika sekmīgi pārsūdzēts un vairs nav spēkā
       appeal_rejected: Apelācija ir noraidīta
       appeal_submitted_at: Apelācija iesniegta
-      appealed_msg: Jūsu apelācija ir iesniegta. Ja tā tiks apstiprināta, jums tiks paziņots.
+      appealed_msg: Tava pārsūdzība ir iesniegta. Ja tā tiks apstiprināta, Tev tiks paziņots.
       appeals:
         submit: Iesniegt apelāciju
       approve_appeal: Apstiprināt apelāciju
@@ -1261,14 +1330,14 @@ lv:
       title_actions:
         delete_statuses: Ziņas noņemšana
         disable: Konta iesaldēšana
-        mark_statuses_as_sensitive: Ziņu atzīmēšana kā sensitīvas
+        mark_statuses_as_sensitive: Ierakstu atzīmēšana kā jūtīgus
         none: Brīdinājums
-        sensitive: Konta atzīmēšana kā sensitīvs
+        sensitive: Konta atzīmēšana kā jūtīgu
         silence: Konta ierobežošana
         suspend: Konta apturēšana
-      your_appeal_approved: Jūsu apelācija ir apstiprināta
+      your_appeal_approved: Tava pārsūdzība tika apstiprināta
       your_appeal_pending: Jūs esat iesniedzis apelāciju
-      your_appeal_rejected: Jūsu apelācija ir noraidīta
+      your_appeal_rejected: Tava pārsūdzība tika noraidīta
   edit_profile:
     basic_information: Pamata informācija
     hint_html: "<strong>Pielāgo, ko cilvēki redz Tavā publiskajā profilā un blakus Taviem ierakstiem.</strong> Ir lielāka iespējamība, ka citi clivēki sekos Tev un mijiedarbosies ar Tevi, ja Tev ir aizpildīts profils un profila attēls."
@@ -1367,7 +1436,7 @@ lv:
       other: Atlasīti visi <strong>%{count}</strong> vienumi, kas atbilst tavam meklēšanas vaicājumam.
       zero: Atlasīts <strong>%{count}</strong> vienumu, kas atbilst tavam meklēšanas vaicājumam.
     cancel: Atcelt
-    changes_saved_msg: Izmaiņas veiksmīgi saglabātas!
+    changes_saved_msg: Izmaiņas sekmīgi saglabātas.
     confirm: Apstiprināt
     copy: Kopēt
     delete: Dzēst
@@ -1399,6 +1468,56 @@ lv:
       merge_long: Saglabāt esošos ierakstus un pievienot jaunus
       overwrite: Pārrakstīt
       overwrite_long: Nomainīt pašreizējos ierakstus ar jauniem
+    overwrite_preambles:
+      blocking_html:
+        one: Tu gatavojies <strong>aizstāt savu lieguma sarakstu</strong> ar līdz <strong>%{count} kontam</strong> no <strong>%{filename}</strong>.
+        other: Tu gatavojies <strong>aizstāt savu lieguma sarakstu</strong> ar līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+        zero: Tu gatavojies <strong>aizstāt savu lieguma sarakstu</strong> ar līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+      bookmarks_html:
+        one: Tu gatavojies <strong>aizstāt savas grāmatzīmes</strong> ar līdz <strong>%{count} ierakstam</strong> no <strong>%{filename}</strong>.
+        other: Tu gatavojies <strong>aizstāt savas grāmatzīmes</strong> ar līdz <strong>%{count} ierakstiem</strong> no <strong>%{filename}</strong>.
+        zero: Tu gatavojies <strong>aizstāt savas grāmatzīmes</strong> ar līdz <strong>%{count} ierakstiem</strong> no <strong>%{filename}</strong>.
+      domain_blocking_html:
+        one: Tu gatavojies <strong>aizstāt savu liegto domēnu sarakstu</strong> ar līdz <strong>%{count} domēnam</strong> no <strong>%{filename}</strong>.
+        other: Tu gatavojies <strong>aizstāt savu liegto domēnu sarakstu</strong> ar līdz <strong>%{count} domēniem</strong> no <strong>%{filename}</strong>.
+        zero: Tu gatavojies <strong>aizstāt savu liegto domēnu sarakstu</strong> ar līdz <strong>%{count} domēniem</strong> no <strong>%{filename}</strong>.
+      following_html:
+        one: Tu gatavojies <strong>sekot</strong> līdz <strong>%{count} kontam</strong> no <strong>%{filename}</strong> un <strong>pārtrauksi sekot citiem</strong>.
+        other: Tu gatavojies <strong>sekot</strong> līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong> un <strong>pārtrauksi sekot citiem</strong>.
+        zero: Tu gatavojies <strong>sekot</strong> līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong> un <strong>pārtrauksi sekot citiem</strong>.
+      lists_html:
+        one: Tu gatavojies <strong>aizstāt savus sarakstus</strong> ar <strong>%{filename}</strong> saturu. Līdz <strong>%{count} kontam</strong> tiks pievienoti jaunajos sarakstos.
+        other: Tu gatavojies <strong>aizstāt savus sarakstus</strong> ar <strong>%{filename}</strong> saturu. Līdz <strong>%{count} kontiem</strong> tiks pievienoti jaunajos sarakstos.
+        zero: Tu gatavojies <strong>aizstāt savus sarakstus</strong> ar <strong>%{filename}</strong> saturu. Līdz <strong>%{count} kontiem</strong> tiks pievienoti jaunajos sarakstos.
+      muting_html:
+        one: Tu gatavojies <strong>aizstāt savu apklusināto kontu sarakstu</strong> ar līdz <strong>%{count} kontam</strong> no <strong>%{filename}</strong>.
+        other: Tu gatavojies <strong>aizstāt savu apklusināto kontu sarakstu</strong> ar līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+        zero: Tu gatavojies <strong>aizstāt savu apklusināto kontu sarakstu</strong> ar līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+    preambles:
+      blocking_html:
+        one: Tu gatavojies <strong>liegt</strong> līdz <strong>%{count} kontam</strong> no <strong>%{filename}</strong>.
+        other: Tu gatavojies <strong>liegt</strong> līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+        zero: Tu gatavojies <strong>liegt</strong> līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+      bookmarks_html:
+        one: Tu gatavojies pievienot līdz <strong>%{count} ierakstam</strong> no <strong>%{filename}</strong> savām <strong>grāmatzīmēm</strong>.
+        other: Tu gatavojies pievienot līdz <strong>%{count} ierakstiem</strong> no <strong>%{filename}</strong> savām <strong>grāmatzīmēm</strong>.
+        zero: Tu gatavojies pievienot līdz <strong>%{count} ierakstiem</strong> no <strong>%{filename}</strong> savām <strong>grāmatzīmēm</strong>.
+      domain_blocking_html:
+        one: Tu gatavojies <strong>liegt</strong> līdz <strong>%{count} domēnam</strong> no <strong>%{filename}</strong>.
+        other: Tu gatavojies <strong>liegt</strong> līdz <strong>%{count} domēniem</strong> no <strong>%{filename}</strong>.
+        zero: Tu gatavojies <strong>liegt</strong> līdz <strong>%{count} domēniem</strong> no <strong>%{filename}</strong>.
+      following_html:
+        one: Tu gatavojies <strong>sekot</strong> līdz <strong>%{count} kontam</strong> no <strong>%{filename}</strong>.
+        other: Tu gatavojies <strong>sekot</strong> līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+        zero: Tu gatavojies <strong>sekot</strong> līdz <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+      lists_html:
+        one: Tu gatavojies pievienot līdz pat <strong>%{count} kontam</strong> no <strong>%{filename}</strong> saviem <strong>sarakstiem</strong>. Tiks izveidoti jauni saraksti, ja nav saraksta, kurā pievienot.
+        other: Tu gatavojies pievienot līdz pat <strong>%{count} kontiem</strong> no <strong>%{filename}</strong> saviem <strong>sarakstiem</strong>. Tiks izveidoti jauni saraksti, ja nav saraksta, kurā pievienot.
+        zero: Tu gatavojies pievienot līdz pat <strong>%{count} kontiem</strong> no <strong>%{filename}</strong> saviem <strong>sarakstiem</strong>. Tiks izveidoti jauni saraksti, ja nav saraksta, kurā pievienot.
+      muting_html:
+        one: Tu gatavojies <strong>apklusināt</strong> līdz pat <strong>%{count} kontam</strong> no <strong>%{filename}</strong>.
+        other: Tu gatavojies <strong>apklusināt</strong> līdz pat <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
+        zero: Tu gatavojies <strong>apklusināt</strong> līdz pat <strong>%{count} kontiem</strong> no <strong>%{filename}</strong>.
     preface: Tu vari ievietot datus, kurus esi izguvis no cita servera, kā, piemēram, cilvēku sarakstu, kuriem Tu seko vai kurus bloķē.
     recent_imports: Nesen importēts
     states:
@@ -1407,7 +1526,7 @@ lv:
       scheduled: Ieplānots
       unconfirmed: Neapstiprināti
     status: Statuss
-    success: Tavi dati tika veiksmīgi augšupielādēti un tiks apstrādāti noteiktajā laikā
+    success: Tavi dati tika sekmīgi augšupielādēti un tiks apstrādāti noteiktajā laikā
     time_started: Sākuma laiks
     titles:
       blocking: Importē bloķētos kontus
@@ -1439,7 +1558,7 @@ lv:
       '604800': 1 nedēļa
       '86400': 1 diena
     expires_in_prompt: Nekad
-    generate: Ģenerēt uzaicinājuma saiti
+    generate: Izveidot uzaicinājuma saiti
     invalid: Šis uzaicinājums nav derīgs
     invited_by: 'Tevi uzaicināja:'
     max_uses:
@@ -1447,7 +1566,7 @@ lv:
       other: "%{count} lietojumi"
       zero: "%{count} lietojumu"
     max_uses_prompt: Nav ierobežojuma
-    prompt: Izveido un kopīgo saites ar citiem, lai piešķirtu piekļuvi šim serverim
+    prompt: Jāizveido un jākopīgo saites ar citiem, lai nodrošinātu piekļuvi šim serverim
     table:
       expires_at: Beidzas
       uses: Lieto
@@ -1464,12 +1583,13 @@ lv:
     description_html: Ja pamani darbības, kuras neatpazīsti, jāapsver iespēja nomainīt savu paroli un iespējot divpakāpju autentifikāciju.
     empty: Nav pieejama autentifikācijas vēsture
     failed_sign_in_html: Neizdevies pieteikšanās mēģinājums ar %{method} no %{ip} (%{browser})
-    successful_sign_in_html: Veiksmīga pieteikšanās ar %{method} no %{ip} (%{browser})
+    successful_sign_in_html: Sekmīga pieteikšanās ar %{method} no %{ip} (%{browser})
     title: Autentifikācijas vēsture
   mail_subscriptions:
     unsubscribe:
       action: Jā, atcelt abonēšanu
       complete: Anulēts
+      confirmation_html: Vai tiešām atteikties no %{type} saņemšanas savā e-pasta adresē %{email} par %{domain} esošo Mastodon? Vienmēr var abonēt no jauna savos <a href="%{settings_path}">e-pasta paziņojumu iestatījumos</a>.
       emails:
         notification_emails:
           favourite: izlases paziņojumu e-pasta ziņojumi
@@ -1477,10 +1597,13 @@ lv:
           follow_request: sekošanas pieprasījumu e-pasta ziņojumi
           mention: pieminēšanas paziņojumu e-pasta ziņojumi
           reblog: pastiprinājumu paziņojumu e-pasta ziņojumi
+      resubscribe_html: Ja abonements tika atcelts kļūdas dēļ, abonēt no jauna var savos <a href="%{settings_path}">e-pasta paziņojumu iestatījumos</a>.
+      success_html: Tu vairs savā e-pasta adresē %{email} nesaņemsi %{type} par %{domain} esošo Mastodon.
       title: Atcelt abonēšanu
   media_attachments:
     validations:
       images_and_video: Nevar pievienot videoklipu tādai ziņai, kura jau satur attēlus
+      not_found: Informācijas nesējs %{ids} nav atrasts vai jau pievienots citam ierakstam
       not_ready: Nevar pievienot failus, kuru apstrāde nav pabeigta. Pēc brīža mēģini vēlreiz!
       too_many: Nevar pievienot vairāk kā 4 failus
   migrations:
@@ -1558,6 +1681,7 @@ lv:
       subject: "%{name} laboja ierakstu"
   notifications:
     administration_emails: Pārvaldītāju e-pasta paziņojumi
+    email_events: E-pasta paziņojumu notikumi
     email_events_hint: 'Atlasi notikumus, par kuriem vēlies saņemt paziņojumus:'
   number:
     human:
@@ -1570,10 +1694,10 @@ lv:
           thousand: K
           trillion: T
   otp_authentication:
-    code_hint: Lai apstiprinātu, ievadi autentifikācijas lietotnes ģenerēto kodu
-    description_html: Jā iespējo <strong>divpakāpju autentifikāciju</strong> ar autentificēšanas lietotni, pieteikšanās laikā būs nepieciešams tālrunis, kurā tiks izveidoti ievadāmie kodi.
+    code_hint: Jāievada autentificētājlietotnes izveidotais kods, lai apstiprinātu
+    description_html: Jā iespējo <strong>divpakāpju autentifikāciju</strong> ar autentificētājlietotni, pieteikšanās laikā būs nepieciešams tālrunis, kurā tiks izveidoti ievadāmie kodi.
     enable: Iespējot
-    instructions_html: "<strong>Skenē šo QR kodu Google Authenticator vai līdzīgā TOTP lietotnē savā tālrunī</strong>. No šī brīža šī lietotne ģenerēs marķierus, kas tev būs jāievada, piesakoties."
+    instructions_html: "<strong>Šis kvadrātods savā tālrunī jānolasa ar Google Authenticator vai līdzīgu TOTP lietotni</strong>. No šī brīža šī lietotne veidos ciparvienības, kas būs jāievada piesakoties."
     manual_instructions: 'Ja nevari noskenēt QR kodu un tas ir jāievada manuāli, šeit ir noslēpums vienkāršā tekstā:'
     setup: Iestatīt
     wrong_code: Ievadītais kods bija nederīgs. Vai servera un ierīces laiks ir pareizs?
@@ -1614,6 +1738,9 @@ lv:
     errors:
       limit_reached: Sasniegts dažādu reakciju limits
       unrecognized_emoji: nav atpazīta emocijzīme
+  redirects:
+    prompt: Ja uzticies šai saitei, jāklikšķina uz tās, lai turpinātu.
+    title: Tu atstāj %{instance}.
   relationships:
     activity: Konta aktivitāte
     confirm_follow_selected_followers: Vai tiešām vēlies sekot atlasītajiem sekotājiem?
@@ -1695,7 +1822,7 @@ lv:
       windows_mobile: Windows Mobile
       windows_phone: Windows Phone
     revoke: Atsaukt
-    revoke_success: Sesija veiksmīgi atsaukta
+    revoke_success: Sesija sekmīgi atsaukta
     title: Sesijas
     view_authentication_history: Skatīt sava konta autentifikācijas vēsturi
   settings:
@@ -1725,9 +1852,13 @@ lv:
   severed_relationships:
     download: Lejupielādēt (%{count})
     event_type:
+      account_suspension: Konta apturēšana (%{target_name})
+      domain_block: Servera apturēšana (%{target_name})
       user_domain_block: Jūs bloķējāt %{target_name}
     lost_followers: Zaudētie sekotāji
     lost_follows: Zaudētie sekojumi
+    preamble: Tu vari zaudēt sekojamos un sekotājus, kad liedz domēnu vai kad satura pārraudzītāji izlemj apturēt attālu serveri. Kad t as notiek, būs iespējams lejupielādēt sarakstus ar pārtrauktajām saiknēm, kurus tad var izpētīt un, iespējams, ievietot citā serverī.
+    purged: Informāciju par šo serveri notīrīja Tava servera pārvaldītāji.
     type: Notikums
   statuses:
     attached:
@@ -1756,10 +1887,10 @@ lv:
       in_reply_not_found: Šķiet, ka ziņa, uz kuru tu mēģini atbildēt, nepastāv.
     over_character_limit: pārsniegts %{max} rakstzīmju ierobežojums
     pin_errors:
-      direct: Ziņojumus, kas ir redzami tikai minētajiem lietotājiem, nevar piespraust
-      limit: Tu jau esi piespraudis maksimālo ziņu skaitu
+      direct: Ierakstus, kas ir redzami tikai pieminētajiem lietotājiem, nevar piespraust
+      limit: Jau ir piesprausts lielākais iespējamais ierakstu skaits
       ownership: Kāda cita ierakstu nevar piespraust
-      reblog: Izceltu ierakstu nevar piespraust
+      reblog: Pastiprinātu ierakstu nevar piespraust
     title: "%{name}: “%{quote}”"
     visibilities:
       direct: Tiešs
@@ -1782,8 +1913,8 @@ lv:
     keep_direct_hint: Nedzēš nevienu tavu tiešo ziņojumu
     keep_media: Saglabāt ziņas ar multivides pielikumiem
     keep_media_hint: Neizdzēš nevienu no tavām ziņām, kurām ir multivides pielikumi
-    keep_pinned: Saglabāt piespraustās ziņas
-    keep_pinned_hint: Nedzēš nevienu tavis piesprausto ziņu
+    keep_pinned: Paturēt piespraustos ierakstus
+    keep_pinned_hint: Neizdzēš nevienu no Tevis piespraustajiem ierakstiem
     keep_polls: Saglabāt aptaujas
     keep_polls_hint: Nedzēš nevienu tavu aptauju
     keep_self_bookmark: Saglabāt ziņas, kuras esi pievienojis grāmatzīmēm
@@ -1801,11 +1932,11 @@ lv:
       '7889238': 3 mēneši
     min_age_label: Vecuma slieksnis
     min_favs: Saglabāt ziņas izlsasē vismaz
-    min_favs_hint: Nedzēš nevienu jūsu ziņu, kas ir saņēmusi vismaz tik daudz izcēlumu. Atstājiet tukšu, lai dzēstu ziņas neatkarīgi no to izcēlumu skaita
+    min_favs_hint: Neizdzēš nevienu no Taviem ierakstiem, kas ir pievienoti šādā daudzumā izlašu. Atstāt tukšu, lai izdzēstu ierakstus neatkarīgi no tā, cik izlasēs tie ir pievienoti
     min_reblogs: Saglabāt ziņas izceltas vismaz
     min_reblogs_hint: Neizdzēš nevienu no tavām ziņām, kas ir izceltas vismaz tik reižu. Atstāj tukšu, lai dzēstu ziņas neatkarīgi no to izcēlumu skaita
   stream_entries:
-    sensitive_content: Sensitīvs saturs
+    sensitive_content: Jūtīgs saturs
   strikes:
     errors:
       too_late: Brīdinājuma apstrīdēšanas laiks ir nokavēts
@@ -1817,6 +1948,7 @@ lv:
     contrast: Mastodon (Augsts kontrasts)
     default: Mastodon (Tumšs)
     mastodon-light: Mastodon (Gaišs)
+    system: Automātisks (ievēro sistēmas izskatu)
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
@@ -1830,31 +1962,45 @@ lv:
   two_factor_authentication:
     add: Pievienot
     disable: Atspējot 2FA
-    disabled_success: Divpakāpju autentifikācija veiksmīgi atspējota
+    disabled_success: Divpakāpju autentificēšanās sekmīgi atspējota
     edit: Labot
     enabled: Divpakāpju autentifikācija ir iespējota
-    enabled_success: Divpakāpju autentifikācija veiksmīgi iespējota
-    generate_recovery_codes: Ģenerēt atkopšanas kodus
-    lost_recovery_codes: Atkopšanas kodi ļauj atgūt piekļuvi tavam kontam, ja pazaudē tālruni. Ja esi pazaudējis atkopšanas kodus, tu vari tos ģenerēt šeit. Tavi vecie atkopšanas kodi tiks anulēti.
+    enabled_success: Divpakāpju autentificēšanās sekmīgi iespējota
+    generate_recovery_codes: Izveidot atkopes kodus
+    lost_recovery_codes: Atkopes kodi ļauj atgūt piekļuvi savam kontam, ja ir pazaudēts tālrunis. Ja ir pazaudēti atkopes kodi, tos var izveidot šeit. Iepriekšējie atkopšanas kodi kļūs nederīgi.
     methods: Divpakāpju veidi
     otp: Autentifikātora lietotne
-    recovery_codes: Veidot atkopšanas kodu rezerves kopijas
-    recovery_codes_regenerated: Atkopšanas kodi veiksmīgi atjaunoti
-    recovery_instructions_html: Ja kādreiz zaudēsi piekļuvi savam tālrunim, vari izmantot kādu no tālāk norādītajiem atkopšanas kodiem, lai atgūtu piekļuvi savam kontam. <strong>Glabā atkopšanas kodus drošībā</strong>. Piemēram, tu vari tos izdrukāt un uzglabāt kopā ar citiem svarīgiem dokumentiem.
+    recovery_codes: Veidot atkopes kodu rezerves kopijas
+    recovery_codes_regenerated: Atkopes kodi sekmīgi izveidoti no jauna
+    recovery_instructions_html: Ja kādreiz zaudēsi piekļuvi savam tālrunim, vari izmantot kādu no zemāk norādītajiem atkopes kodiem, lai atgūtu piekļuvi savam kontam. <strong>Atkpes kodi jātur drošībā</strong>. Piemēram, tos var izdrukāt un glabāt kopā ar citiem svarīgiem dokumentiem.
     webauthn: Drošības atslēgas
   user_mailer:
+    announcement_published:
+      description: "%{domain} pārvaldītāji veic paziņojumu:"
+      subject: Pakalpojuma paziņojums
+      title: "%{domain} pakalpojuma paziņojums"
     appeal_approved:
       action: Konta iestatījumi
-      explanation: Apelācija par brīdinājumu jūsu kontam %{strike_date}, ko iesniedzāt %{appeal_date}, ir apstiprināta. Jūsu konts atkal ir labā stāvoklī.
-      subject: Jūsu %{date} apelācija ir apstiprināta
+      explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, ir apstiprināta. Tavs konts atkal ir labā stāvoklī.
+      subject: Tava %{date} iesniegtā pārsūdzība tika apstiprināta
+      subtitle: Tavs konts atkal ir labā stāvoklī.
       title: Apelācija apstiprināta
     appeal_rejected:
-      explanation: Apelācija par brīdinājumu jūsu kontam %{strike_date}, ko iesniedzāt %{appeal_date}, ir noraidīta.
-      subject: Jūsu %{date} apelācija ir noraidīta
+      explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, tika noraidīta.
+      subject: Tava %{date} iesniegta pārsūdzība tika noraidīta
+      subtitle: Tava pārsūdzība tika noraidīta.
       title: Apelācija noraidīta
     backup_ready:
+      explanation: Tu pieprasīji pilnu sava Mastodon konta rezerves kopiju.
+      extra: Tā tagad ir gatava lejupielādei.
       subject: Tavs arhīvs ir gatavs lejupielādei
       title: Arhīva līdzņemšana
+    failed_2fa:
+      details: 'Šeit ir informācija par pieteikšanās mēģinājumu:'
+      explanation: Kāds mēģināja pieteikties Tavā kontā, bet norādīja nederīgu otro autentificēšanās soli.
+      further_actions_html: Ja tas nebiji Tu, mēs iesakām nekavējoties %{action}, jo var būt noticis drošības pārkāpums.
+      subject: Otrās pakāpes autentificēšanās atteice
+      title: Neizdevās otrās pakāpes autentificēšanās
     suspicious_sign_in:
       change_password: mainīt paroli
       details: 'Šeit ir pieteikšanās izvērsums:'
@@ -1865,8 +2011,7 @@ lv:
     terms_of_service_changed:
       agreement: Ar %{domain} izmantošanas tuprināšanu tiek piekrists šiem noteikumiem. Ja ir iebildumi pret atjauninātajiem noteikumiem, savu piekrišanu var atcelt jebkurā laikā ar sava konta izdzēšanu.
       changelog: 'Šeit īsumā ir aprakstīts, ko šis atjauninājums nozīmē:'
-      description: 'Šis e-pasta ziņojums tika saņemts, jo mēs veicam dažas izmaiņas savos pakalpojuma izmantošanas noteikumos %{domain}. Mēs aicinām pārskatīt pilnus atjauninātos noteikumus šeit:'
-      description_html: Šis e-pasta ziņojums tika saņemts, jo mēs veicam dažas izmaiņas savos pakalpojuma izmantošanas noteikumos %{domain}. Mēs aicinām pārskatīt <a href="%{path}" target="_blank">pilnus atjauninātos noteikumus šeit</a>.
+      description: 'Šis e-pasta ziņojums tika saņemts, jo mēs veicam dažas izmaiņas savos pakalpojuma izmantošanas noteikumos %{domain}. Šie atjauninājumi stāsies spēkā %{date}. Mēs aicinām pārskatīt pilnus atjauninātos noteikumus šeit:'
       sign_off: "%{domain} komanda"
       subject: Mūsu pakalpojuma izmantošanas noteikumu atjauninājumi
       subtitle: Mainās %{domain} pakalpojuma izmantošanas noteikumi
@@ -1878,10 +2023,10 @@ lv:
         spam: Spams
         violation: Saturs pārkāpj šādas kopienas pamatnostādnes
       explanation:
-        delete_statuses: Tika konstatēts, ka dažas no tavām ziņām pārkāpj vienu vai vairākas kopienas vadlīnijas, un rezultātā %{instance} moderatori tās noņēma.
+        delete_statuses: Tika noteikts, ka daži no Taviem ierakstiem pārkāpj vienu vai vairākas kopienas vadlīnijas, tādējādi tos noņēma %{instance} satura pārraudzītāji.
         disable: Tu vairs nevari izmantot savu kontu, taču tavs profils un citi dati paliek neskarti. Tu vari pieprasīt savu datu dublējumu, mainīt konta iestatījumus vai dzēst kontu.
-        mark_statuses_as_sensitive: "%{instance} moderatori dažus no Taviem ierakstiem ir atzīmējuši kā jutīgus. Tas nozīmē, ka cilvēkiem būs jāpiesit ierakstos esošajiem informāijas nesējiem, pirms tiek attēlots priekšskatījums. Tu arī pats vari atzīmēt informācijas nesēju kā jutīgu, kad nākotnē tādu ievietosi."
-        sensitive: No šī brīža visi augšupielādētie multivides faili tiks atzīmēti kā sensitīvi un paslēpti aiz klikšķa brīdinājuma.
+        mark_statuses_as_sensitive: "%{instance} satura pārraudzītāji dažus no Taviem ierakstiem ir atzīmējuši kā jūtīgus. Tas nozīmē, ka cilvēkiem būs jāpiesit ierakstos esošajiem informāijas nesējiem, pirms tiek attēlots to priekšskatījums. Tu pats vari atzīmēt informācijas nesēju kā jūtīgu, kad nākotnē tādu ievietosi."
+        sensitive: Turpmāk visi augšupielādētās informācijas nesēju datnes tiks atzīmētas kā jūtīgas un paslēptas aiz klikšķināma brīdinājuma.
         silence: Tu joprojām vari izmantot savu kontu, taču tikai tie cilvēki, kuri jau tev seko, redzēs tavas ziņas šajā serverī, un tev var tikt liegtas dažādas atklāšanas funkcijas. Tomēr citi joprojām var tev manuāli sekot.
         suspend: Tu vairs nevari izmantot savu kontu, un tavs profils un citi dati vairs nav pieejami. Tu joprojām vari pieteikties, lai pieprasītu savu datu dublēšanu, līdz dati tiks pilnībā noņemti aptuveni 30 dienu laikā, taču mēs saglabāsim dažus pamata datus, lai neļautu tev izvairīties no apturēšanas.
       reason: 'Iemesls:'
@@ -1889,17 +2034,17 @@ lv:
       subject:
         delete_statuses: Tavas ziņas %{acct} tika noņemtas
         disable: Tavs konts %{acct} tika iesaldēts
-        mark_statuses_as_sensitive: Tavas ziņas vietnē %{acct} ir atzīmētas kā sensitīvas
+        mark_statuses_as_sensitive: Tavi ieraksti %{acct} ir atzīmēti kā jūtīgi
         none: Brīdinājums par %{acct}
-        sensitive: Tavas ziņas vietnē %{acct} turpmāk tiks atzīmētas kā sensitīvas
+        sensitive: Tavi ieraksti %{acct} turpmāk tiks atzīmēti kā jūtīgi
         silence: Tavs konts %{acct} tika ierobežots
         suspend: Tava konta %{acct} darbība ir apturēta
       title:
         delete_statuses: Izdzēstās ziņas
         disable: Konts iesaldēts
-        mark_statuses_as_sensitive: Ziņas ir atzīmēts kā sensitīvas
+        mark_statuses_as_sensitive: Ieraksti atzīmēti kā jūtīgi
         none: Brīdinājums
-        sensitive: Konts ir atzīmēts kā sensitīvs
+        sensitive: Konts ir atzīmēts kā jūtīgs
         silence: Konts ierobežots
         suspend: Konts apturēts
     welcome:
@@ -1956,13 +2101,13 @@ lv:
     add: Pievienot jaunu drošības atslēgu
     create:
       error: Pievienojot drošības atslēgu, radās problēma. Lūdzu mēģini vēlreiz.
-      success: Tava drošības atslēga tika veiksmīgi pievienota.
+      success: Tava drošības atslēga tika sekmīgi pievienota.
     delete: Dzēst
     delete_confirmation: Vai tiešām vēlies dzēst šo drošības atslēgu?
     description_html: Ja iespējosi <strong>drošības atslēgas autentifikāciju</strong>, piesakoties būs jāizmanto viena no tavām drošības atslēgām.
     destroy:
       error: Dzēšot tavu drošības atslēgu, radās problēma. Lūdzu mēģini vēlreiz.
-      success: Tava drošības atslēga tika veiksmīgi izdēsta.
+      success: Tava drošības atslēga tika sekmīgi izdēsta.
     invalid_credential: Nederīga drošības atslēga
     nickname_hint: Ievadi savas jaunās drošības atslēgas segvārdu
     not_enabled: Tu vel neesi iespējojis WebAuthn
diff --git a/config/locales/ms.yml b/config/locales/ms.yml
index 94cbca57fd..68f4e872b6 100644
--- a/config/locales/ms.yml
+++ b/config/locales/ms.yml
@@ -28,6 +28,7 @@ ms:
       created_msg: Catatan penyederhanaan telah berjaya dicipta!
       destroyed_msg: Catatan penyederhanaan telah berjaya dipadam!
     accounts:
+      add_email_domain_block: Sekat domain e-mel
       approve: Luluskan
       approved_msg: Berjaya meluluskan permohonan pendaftaran %{username}
       are_you_sure: Adakah anda pasti?
@@ -146,7 +147,7 @@ ms:
       suspension_irreversible: Data akaun ini telah dipadam secara kekal. Anda boleh nyahgantungkannya untuk membuatkan akaun ini boleh digunakan semula tetapi data lama tidak akan diperolehi.
       suspension_reversible_hint_html: Akaun ini telah digantung, dan datanya akan dibuang pada %{date}. Sebelum tarikh itu, akaun ini boleh diperoleh semula tanpa kesan buruk. Jika anda mahu memadamkan kesemua data akaun ini serta-merta, anda boleh melakukannya di bawah.
       title: Akaun
-      unblock_email: Menyahsekat alamat e-mel
+      unblock_email: Nyahsekat alamat e-mel
       unblocked_email_msg: Alamat e-mel %{username} berjaya dinyahsekat
       unconfirmed_email: E-mel belum disahkan
       undo_sensitized: Nyahtanda sensitif
@@ -169,17 +170,21 @@ ms:
         confirm_user: Sahkan Pengguna
         create_account_warning: Cipta Amaran
         create_announcement: Cipta Pengumuman
+        create_canonical_email_block: Cipta Penyekatan E-mel
         create_custom_emoji: Cipta Emoji Tersendiri
         create_domain_allow: Cipta Pelepasan Domain
         create_domain_block: Cipta Penyekatan Domain
+        create_email_domain_block: Cipta Penyekatan Domain E-mel
         create_ip_block: Cipta peraturan alamat IP
         create_unavailable_domain: Cipta Domain Tidak Tersedia
         create_user_role: Cipta Peranan
         demote_user: Turunkan Taraf Pengguna
         destroy_announcement: Padam Pengumuman
+        destroy_canonical_email_block: Padam Penyekatan E-mel
         destroy_custom_emoji: Padam Emoji Tersendiri
         destroy_domain_allow: Padam Pelepasan Domain
         destroy_domain_block: Padam Penyekatan Domain
+        destroy_email_domain_block: Padam Penyekatan Domain E-mel
         destroy_instance: Padamkan Domain
         destroy_ip_block: Padam peraturan alamat IP
         destroy_status: Padam Hantaran
@@ -203,7 +208,7 @@ ms:
         silence_account: Diamkan Akaun
         suspend_account: Gantungkan Akaun
         unassigned_report: Menyahtugaskan Laporan
-        unblock_email_account: Menyahsekat alamat e-mel
+        unblock_email_account: Nyahsekat alamat e-mel
         unsensitive_account: Nyahtanda media di akaun anda sebagai sensitif
         unsilence_account: Nyahdiamkan Akaun
         unsuspend_account: Nyahgantungkan Akaun
@@ -756,7 +761,7 @@ ms:
         remove_from_report: Alih keluar daripada laporan
         report: Laporan
       deleted: Dipadamkan
-      favourites: Gemaran
+      favourites: Sukaan
       history: Sejarah versi
       in_reply_to: Membalas kepada
       language: Bahasa
@@ -861,7 +866,7 @@ ms:
         no_status_selected: Tiada pos sohor kini ditukar kerana tiada yang dipilih
         not_discoverable: Pengarang tidak mengikut serta untuk dapat ditemui
         shared_by:
-          other: Dikongsi dan digemari %{friendly_count} kali
+          other: Dikongsi atau disukai %{friendly_count} kali
         title: Hantaran hangat
       tags:
         current_score: Markah semasa %{score}
@@ -1144,7 +1149,7 @@ ms:
     csv: CSV
     domain_blocks: Domain disekat
     lists: Senarai
-    mutes: Awak bisu
+    mutes: Redaman anda
     storage: Storan Media
   featured_tags:
     add_new: Tambah baharu
@@ -1235,10 +1240,11 @@ ms:
       domain_blocking: Mengimport domain yang disekat
       following: Mengimport akaun diikuti
       lists: Mengimport senarai
-      muting: Mengimport akaun diredam
+      muting: Mengimport akaun teredam
     type: Jenis import
     type_groups:
       constructive: Ikutan & Penanda Halaman
+      destructive: Sekatan dan redaman
     types:
       blocking: Senarai menyekat
       bookmarks: Penanda buku
@@ -1283,6 +1289,9 @@ ms:
     unsubscribe:
       action: Ya, nyahlanggan
       complete: Menyahlanggan
+      emails:
+        notification_emails:
+          favourite: emel pemberitahuan sukaan
       title: Hentikan langganan
   media_attachments:
     validations:
@@ -1334,9 +1343,9 @@ ms:
       sign_up:
         subject: "%{name} telah mendaftar"
     favourite:
-      body: 'Pos anda telah digemari oleh %{name}:'
-      subject: "%{name} menggemari siaran anda"
-      title: Kegemaran baru
+      body: 'Hantaran anda disukai oleh %{name}:'
+      subject: "%{name} menyukai hantaran anda"
+      title: Sukaan baharu
     follow:
       body: "%{name} kini mengikuti anda!"
       subject: "%{name} kini mengikuti anda"
@@ -1558,7 +1567,7 @@ ms:
     enabled_hint: Memadamkan pos anda secara automatik setelah mereka mencapai ambang umur yang ditentukan, melainkan ia sepadan dengan salah satu pengecualian di bawah
     exceptions: Pengecualian
     explanation: Oleh kerana pemadaman pos adalah operasi yang mahal, ini dilakukan perlahan-lahan dari semasa ke semasa apabila server tidak sibuk. Atas sebab ini, pos anda mungkin dipadamkan seketika selepas ia mencapai ambang umur.
-    ignore_favs: Abaikan kegemaran
+    ignore_favs: Abaikan sukaan
     ignore_reblogs: Abaikan rangsangan
     interaction_exceptions: Pengecualian berdasarkan interaksi
     interaction_exceptions_explanation: Sila ambil perhatian bahawa tiada jaminan untuk pos dipadamkan jika ia berada di bawah ambang kegemaran atau tingkatkan selepas sekali melepasinya.
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 9f05104153..5acd93ca13 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -309,6 +309,7 @@ nl:
       title: Auditlog
       unavailable_instance: "(domeinnaam niet beschikbaar)"
     announcements:
+      back: Terug naar mededelingen
       destroyed_msg: Verwijderen van mededeling geslaagd!
       edit:
         title: Mededeling bewerken
@@ -317,6 +318,10 @@ nl:
       new:
         create: Mededeling aanmaken
         title: Nieuwe mededeling
+      preview:
+        disclaimer: Omdat gebruikers zich niet voor deze e-mails kunnen afmelden, moeten e-mailmeldingen worden beperkt tot belangrijke aankondigingen, zoals het lekken van gebruikersgegevens of meldingen over het sluiten van deze server.
+        explanation_html: 'De e-mail wordt verzonden naar <strong>%{display_count} gebruikers</strong>. De volgende tekst wordt in het bericht opgenomen:'
+        title: Voorbeeld van mededeling
       publish: Inschakelen
       published_msg: Publiceren van mededeling geslaagd!
       scheduled_for: Ingepland voor %{time}
@@ -475,6 +480,36 @@ nl:
       new:
         title: Domeinblokkades importeren
       no_file: Geen bestand geselecteerd
+    fasp:
+      debug:
+        callbacks:
+          created_at: Aangemaakt op
+          delete: Verwijderen
+          ip: IP-adres
+          request_body: Body opvragen
+          title: Callbacks debuggen
+      providers:
+        active: Ingeschakeld
+        base_url: Basis-URL
+        callback: Callback
+        delete: Verwijderen
+        edit: Aanbieder bewerken
+        finish_registration: Registratie voltooien
+        name: Naam
+        providers: Aanbieders
+        public_key_fingerprint: Vingerafdruk van publieke sleutel
+        registration_requested: Registratie aangevraagd
+        registrations:
+          confirm: Bevestigen
+          description: Je hebt een registratie ontvangen van een FASP. Weiger deze als je dit niet hebt aangevraagd. Heb je dit wel aangevraagd, vergelijk dan zorgvuldig de naam en de vingerafdruk van de sleutel voordat je de registratie bevestigt.
+          reject: Afwijzen
+          title: FASP-registratie bevestigen
+        save: Opslaan
+        select_capabilities: Mogelijkheden selecteren
+        sign_in: Inloggen
+        status: Status
+        title: Fediverse Auxiliary Service Providers
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Deze aanbevolen accounts helpen nieuwe gebruikers snel interessante inhoud</strong>te vinden. Wanneer een gebruiker niet met andere gebruikers genoeg interactie heeft gehad om gepersonaliseerde aanbevelingen te krijgen, worden in plaats daarvan deze accounts aanbevolen. Deze accounts worden dagelijks opnieuw berekend met behulp van accounts met het hoogste aantal recente interacties en het hoogste aantal lokale volgers in een bepaalde taal."
       language: Voor taal
@@ -939,6 +974,7 @@ nl:
         chance_to_review_html: "<strong>De gegenereerde gebruiksvoorwaarden worden niet automatisch gepubliceerd.</strong> Je krijgt de gelegenheid om de resultaten eerst te bekijken. Vul de benodigde gegevens in om verder te gaan."
         explanation_html: Het sjabloon voor de gebruiksvoorwaarden is uitsluitend bedoeld voor informatieve doeleinden en mag niet worden opgevat als juridisch advies over welk onderwerp dan ook. Raadpleeg een eigen juridisch adviseur over jouw situatie en voor specifieke juridische vragen.
         title: Gebruiksvoorwaarden instellen
+      going_live_on_html: Actueel met ingang van %{date}
       history: Geschiedenis
       live: Actueel
       no_history: Er zijn nog geen opgeslagen wijzigingen van de gebruiksvoorwaarden.
@@ -1904,6 +1940,10 @@ nl:
     recovery_instructions_html: Wanneer je ooit de toegang verliest tot jouw telefoon, kan je met behulp van een van de herstelcodes hieronder opnieuw toegang krijgen tot jouw account. <strong>Zorg ervoor dat je de herstelcodes op een veilige plek bewaart</strong>. Je kunt ze bijvoorbeeld printen en ze samen met andere belangrijke documenten bewaren.
     webauthn: Beveiligingssleutels
   user_mailer:
+    announcement_published:
+      description: 'De beheerders van %{domain} doen een mededeling:'
+      subject: Service-aankondiging
+      title: "%{domain} service aankondiging"
     appeal_approved:
       action: Accountinstellingen
       explanation: Het bezwaar tegen een door een moderator vastgestelde overtreding van jou op %{strike_date}, ingediend op %{appeal_date}, is goedgekeurd. De eerder vastgestelde overtreding is hierbij niet langer geldig.
@@ -1936,8 +1976,8 @@ nl:
     terms_of_service_changed:
       agreement: Door %{domain} te blijven gebruiken, ga je akkoord met deze voorwaarden. Als je het niet eens bent met de bijgewerkte voorwaarden, kun je je overeenkomst met %{domain} op elk gewenst moment beëindigen door je account te verwijderen.
       changelog: 'In een oogopslag betekent deze update voor jou:'
-      description: 'Je ontvangt dit bericht omdat we enkele wijzigingen aanbrengen in onze gebruiksvoorwaarden op %{domain}. We raden je aan om de bijgewerkte voorwaarden hier volledig te bekijken:'
-      description_html: Je ontvangt dit bericht omdat we enkele wijzigingen aanbrengen in onze gebruiksvoorwaarden op %{domain}. We raden je aan om de bijgewerkte <a href="%{path}" target="_blank">voorwaarden hier volledig te bestuderen</a>.
+      description: 'Je ontvangt dit bericht, omdat we enkele wijzigingen aanbrengen in onze gebruiksvoorwaarden op %{domain}. Deze aanpassingen worden van kracht op %{date}. We raden je aan om de bijgewerkte voorwaarden hier volledig te bekijken:'
+      description_html: Je ontvangt dit bericht, omdat we enkele wijzigingen aanbrengen in onze gebruiksvoorwaarden op %{domain}. Deze aanpassingen worden van kracht op <strong>%{date}</strong>. We raden je aan om de <a href="%{path}" target="_blank">bijgewerkte voorwaarden hier</a> volledig te bestuderen.
       sign_off: Het %{domain}-team
       subject: Onze bijgewerkte gebruiksvoorwaarden
       subtitle: De gebruiksvoorwaarden van %{domain} veranderen
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index b7982e93ca..bcdcf01d9a 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -1936,8 +1936,6 @@ nn:
     terms_of_service_changed:
       agreement: Viss du held fram å bruka %{domain}, seier du deg einig i vilkåra. Viss du er usamd i dei oppdaterte vilkåra, kan du slutta å bruka %{domain} når du vil ved å sletta brukarkontoen din.
       changelog: 'Denne oppdateringa, kort fortalt:'
-      description: 'Du får denne eposten fordi me har endra bruksvilkåra på %{domain}. Det er fint om du ser gjennom endringane her:'
-      description_html: Du får denne eposten fordi me endrar bruksvilkåra på %{domain}. Me oppmodar deg til å lesa gjennom <a href="%{path}" target="_blank">dei oppdaterte bruksvilkåra her</a>.
       sign_off: Folka på %{domain}
       subject: Endra bruksvilkår
       subtitle: Bruksvilkåra på %{domain} er endra
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 2529fafb4a..d70e60df0a 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -773,6 +773,7 @@
       batch:
         remove_from_report: Fjern fra rapport
         report: Rapport
+      contents: Innhold
       deleted: Slettet
       favourites: Favoritter
       history: Versjonshistorikk
@@ -841,8 +842,25 @@
         action: Sjekk her for mer informasjon
         message_html: "<strong>Objektlagringen din er feilkonfigurert. Personvernet til brukerne dine er i fare.</strong>"
     tags:
+      moderation:
+        pending_review: Avventer gjennomgang
+        reviewed: Gjennomgått
+        title: Status
+      name: Navn
+      newest: Nyeste
+      oldest: Eldst
+      reset: Tilbakestill
       review: Gjennomgangsstatus
+      search: Søk
+      title: Emneknagger
       updated_msg: Emneknagg innstillinger vellykket oppdatert
+    terms_of_service:
+      changelog: Hva har blitt endret
+      current: Nåværende
+      draft: Kladd
+      history: Historikk
+      publish: Publiser
+      title: Bruksvilkår
     title: Administrasjon
     trends:
       allow: Tillat
@@ -1034,6 +1052,7 @@
     migrate_account_html: Hvis du ønsker å henvise denne kontoen til en annen, kan du <a href="%{path}">konfigurere det her</a>.
     or_log_in_with: Eller logg inn med
     progress:
+      confirm: Bekreft E-postadressen
       details: Dine opplysninger
       review: Vår gjennomgang
       rules: Godta regler
@@ -1569,6 +1588,7 @@
     import: Importér
     import_and_export: Importer og eksporter
     migrate: Kontomigrering
+    notifications: E-postbeskjeder
     preferences: Innstillinger
     profile: Profil
     relationships: Følginger og følgere
@@ -1576,6 +1596,8 @@
     strikes: Modereringsadvarsler
     two_factor_authentication: Tofaktorautentisering
     webauthn_authentication: Sikkerhetsnøkler
+  severed_relationships:
+    type: Hendelse
   statuses:
     attached:
       audio:
@@ -1654,6 +1676,8 @@
       too_late: Det er for sent å anke denne advarselen
   tags:
     does_not_match_previous_name: samsvarer ikke med det forrige navnet
+  terms_of_service:
+    title: Bruksvilkår
   themes:
     contrast: Mastodon (Høykontrast)
     default: Mastodon
@@ -1762,6 +1786,7 @@
       follows_view_more: Vis flere personer å følge
       hashtags_title: Populære emneknagger
       hashtags_view_more: Vis flere populære emneknagger
+      post_action: Sett sammen
       post_step: Si hallo til verdenen med tekst, bilder, videoer, eller meningsmålinger.
       post_title: Lag ditt første innlegg
       share_action: Del
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index e2e2860059..50f39591af 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -1247,6 +1247,7 @@ pl:
     too_fast: Zbyt szybko przesłano formularz, spróbuj ponownie.
     use_security_key: Użyj klucza bezpieczeństwa
     user_agreement_html: Przeczytałem i akceptuję <a href="%{terms_of_service_path}" target="_blank">warunki korzystania z usługi</a> oraz <a href="%{privacy_policy_path}" target="_blank">politykę prywatności</a>
+    user_privacy_agreement_html: Przeczytałem/am/o i akceptuję <a href="%{privacy_policy_path}" target="_blank">politykę prywatności</a>
   author_attribution:
     example_title: Przykładowy tekst
     hint_html: Piszesz wiadomości albo bloga poza Mastodonem? Kontroluj jak będą ci przypisywane podczas dizielenia się nimi na Mastodonie.
@@ -2021,8 +2022,6 @@ pl:
     terms_of_service_changed:
       agreement: Kontynuując używanie %{domain}, zgadzasz się na te warunki. Jeśli nie zgadzasz się ze zaktualizowanymi warunkami, możesz wypowiedzieć umowę z %{domain} w dowolnym momencie, usuwając swoje konto.
       changelog: 'W skrócie oto co oznacza dla Ciebie ta aktualizacja:'
-      description: 'Otrzymujesz ten e-mail, ponieważ wprowadzamy pewne zmiany w naszym regulaminie usługi w %{domain}. Zachęcamy Cię do pełnego zapoznania się z aktualnymi warunkami:'
-      description_html: Otrzymujesz ten e-mail, ponieważ wprowadzamy pewne zmiany w naszym regulaminie usługi w %{domain}. Zachęcamy do zapoznania się z aktualnymi warunkami <a href="%{path}" target="_blank">w całości tutaj</a>.
       sign_off: Zespół %{domain}
       subject: Aktualizacja warunków korzystania z usług
       subtitle: Warunki korzystania z %{domain} zmieniają się
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index fe10dd6462..a2e142ab55 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -142,7 +142,7 @@ pt-BR:
         only_password: Apenas senha
         password_and_2fa: Senha e autenticação de dois fatores
       sensitive: Sensíveis
-      sensitized: marcadas como sensíveis
+      sensitized: Marcadas como sensíveis
       shared_inbox_url: Link da caixa de entrada compartilhada
       show:
         created_reports: Denúncias criadas
@@ -309,6 +309,7 @@ pt-BR:
       title: Auditar histórico
       unavailable_instance: "(nome de domínio indisponível)"
     announcements:
+      back: Voltar aos anúncios
       destroyed_msg: Anúncio excluído!
       edit:
         title: Editar anúncio
@@ -317,6 +318,9 @@ pt-BR:
       new:
         create: Criar anúncio
         title: Novo anúncio
+      preview:
+        explanation_html: 'Esse e-mail será enviado a <strong>%{display_count} usuários</strong>. O texto a seguir será incluído ao e-mail:'
+        title: Visualizar anúncio
       publish: Publicar
       published_msg: Anúncio publicado!
       scheduled_for: Agendado para %{time}
@@ -475,6 +479,36 @@ pt-BR:
       new:
         title: Importar bloqueio de domínios
       no_file: Nenhum arquivo selecionado
+    fasp:
+      debug:
+        callbacks:
+          created_at: Criado em
+          delete: Apagar
+          ip: Endereço de IP
+          request_body: Corpo da solicitação
+          title: Depurar Callbacks
+      providers:
+        active: Ativo
+        base_url: URL Base
+        callback: Callback
+        delete: Apagar
+        edit: Editar provedor
+        finish_registration: Finalizar o cadastro
+        name: Nome
+        providers: Provedores
+        public_key_fingerprint: Impressão digital de chave pública
+        registration_requested: Cadastro solicitado
+        registrations:
+          confirm: Confirmar
+          description: Você recebeu um registro de um FASP. Rejeite se você não tiver iniciado isso. Se você iniciou isso, compare cuidadosamente o nome e a impressão digital da chave antes de confirmar o registro.
+          reject: Rejeitar
+          title: Confirmar o registro FASP
+        save: Salvar
+        select_capabilities: Selecionar recursos
+        sign_in: Entrar
+        status: Estado
+        title: Provedores de serviços auxiliares do Fediverso
+      title: FASP
     follow_recommendations:
       description_html: "<strong>A recomendação de contas ajuda os novos usuários a encontrar rapidamente conteúdo interessante</strong>. Quando um usuário ainda não tiver interagido o suficiente para gerar recomendações de contas, essas contas serão recomendadas. Essas recomendações são recalculadas diariamente a partir de uma lista de contas com alto engajamento e maior número de seguidores locais em uma dada língua."
       language: Na língua
@@ -937,6 +971,26 @@ pt-BR:
       generates:
         action: Gerar
         chance_to_review_html: "<strong>Os termos de serviço gerado não será publicado automaticamente.</strong>Você terá uma chance de revisar os resultados. Preencha os detalhes necessários para continuar"
+        explanation_html: O modelo de termos de serviço fornecido é apenas para fins informativos e não deve ser interpretado como aconselhamento jurídico sobre qualquer assunto. Consulte seu próprio advogado para esclarecer sua situação e dúvidas jurídicas específicas.
+        title: Configuração dos Termos de Serviço
+      going_live_on_html: Em vigor a partir de %{date}
+      history: Histórico
+      live: Em vigor
+      no_history: Ainda não há alterações registradas nos termos de serviço.
+      no_terms_of_service_html: Atualmente, você não tem nenhum termo de serviço configurado. Os termos de serviço servem para fornecer clareza e protegê-lo de possíveis responsabilidades em disputas com seus usuários.
+      notified_on_html: Usuários notificados em %{date}
+      notify_users: Notificar usuários
+      preview:
+        explanation_html: 'O e-mail será enviado para <strong>%{display_count} usuários</strong> que se inscreveram antes de %{date}. O seguinte texto será incluído no e-mail:'
+        send_preview: Enviar pŕevia para %{email}
+        send_to_all:
+          one: Enviar %{display_count} email
+          other: Enviar %{display_count} emails
+        title: Prévia da notificação dos termos de serviço
+      publish: Publicar
+      published_on_html: Publicado em %{date}
+      save_draft: Salvar rascunho
+      title: Termos de Serviço
     title: Administração
     trends:
       allow: Permitir
@@ -1168,6 +1222,7 @@ pt-BR:
     set_new_password: Definir uma nova senha
     setup:
       email_below_hint_html: Verifique a sua pasta de spam, ou solicite outra. Você pode corrigir o seu endereço de e-mail se estiver errado.
+      email_settings_hint_html: Clique no link que enviamos para %{email} para começar a usar o Mastodon. Estaremos esperando aqui.
       link_not_received: Não recebeu um link?
       new_confirmation_instructions_sent: Você receberá um novo e-mail com o link de confirmação em alguns minutos!
       title: Verifique sua caixa de entrada
@@ -1176,6 +1231,7 @@ pt-BR:
       title: Entrar em %{domain}
     sign_up:
       manual_review: Inscrições no %{domain} passam pela revisão manual dos nossos moderadores. Para nos ajudar a processar o seu cadastro, escreva um pouco sobre você e por que você quer uma conta no %{domain}.
+      preamble: Com uma conta neste servidor do Mastodon, você poderá seguir qualquer outra pessoa no fediverso, independentemente de onde a conta dela esteja hospedada.
       title: Então vamos lá criar uma conta em %{domain}.
     status:
       account_status: Status da conta
@@ -1187,6 +1243,8 @@ pt-BR:
       view_strikes: Veja os avisos anteriores em relação à sua conta
     too_fast: O formulário foi enviado muito rapidamente, tente novamente.
     use_security_key: Usar chave de segurança
+    user_agreement_html: Eu li e concordo com os <a href="%{terms_of_service_path}" target="_blank">termos de serviço</a> e <a href="%{privacy_policy_path}" target="_blank">política de privacidade</a>.
+    user_privacy_agreement_html: Eu li e concordo com a <a href="%{privacy_policy_path}" target="_blank">política de privacidade</a>.
   author_attribution:
     example_title: Texto de amostra
     hint_html: Você está escrevendo notícias ou artigos de blogs fora do Mastodon? Controle como você é credenciado quando eles forem compartilhados no Mastodon.
@@ -1401,7 +1459,31 @@ pt-BR:
       domain_blocking_html:
         one: Você está prestes a <strong>substituir seu bloco de lista do domínio</strong> com <strong>%{count} domínio</strong> de <strong>%{filename}</strong>.
         other: Você está prestes a <strong>substituir sua lista de bloqueio de domínio</strong> com <strong>domínios%{count}</strong> de <strong>%{filename}</strong>.
+      following_html:
+        one: Você está prestes a <strong>seguir</strong> <strong>%{count} contas</strong> de <strong>%{filename}</strong> e <strong>parar de seguir todos os outros</strong>.
+        other: Você está prestes a <strong>seguir</strong> até <strong>%{count} contas</strong> de <strong>%{filename}</strong> e <strong>deixar de seguir qualquer outra pessoa</strong>.
+      lists_html:
+        one: Você está prestes a <strong>substituir suas listas</strong> pelo conteúdo de <strong>%{filename}</strong>. Até <strong>%{count} conta</strong> será adicionada às novas listas.
+        other: Você está prestes a <strong>substituir suas listas</strong> pelo conteúdo de <strong>%{filename}</strong>. Até <strong>%{count} contas</strong> serão adicionadas às novas listas.
+      muting_html:
+        one: Você está prestes a <strong>substituir sua lista de contas silenciadas</strong> por até <strong>%{count} conta</strong> de <strong>%{filename}</strong>.
+        other: Você está prestes a <strong>substituir sua lista de contas silenciadas</strong> por até <strong>%{count} contas</strong> de <strong>%{filename}</strong>.
     preambles:
+      blocking_html:
+        one: Você está prestes a <strong>bloquear</strong> até <strong>%{count} conta</strong> de <strong>%{filename}</strong>.
+        other: Você está prestes a <strong>bloquear</strong> até <strong>%{count} contas</strong> de <strong>%{filename}</strong>.
+      bookmarks_html:
+        one: Você está prestes a adicionar até <strong>%{count} post</strong> de <strong>%{filename}</strong> aos seus <strong>favoritos</strong>.
+        other: Você está prestes a adicionar até <strong>%{count} posts</strong> de <strong>%{filename}</strong> aos seus <strong>favoritos</strong>.
+      domain_blocking_html:
+        one: Você está prestes a <strong>bloquear</strong> até <strong>%{count} domínio</strong> de <strong>%{filename}</strong>.
+        other: Você está prestes a <strong>bloquear</strong> até <strong>%{count} domínios</strong> de <strong>%{filename}</strong>.
+      following_html:
+        one: Você está prestes a <strong>seguir</strong> até <strong>%{count} conta</strong> de <strong>%{filename}</strong>.
+        other: Você está prestes a <strong>seguir</strong> até <strong>%{count} contas</strong> de <strong>%{filename}</strong>.
+      lists_html:
+        one: Você está prestes a adicionar até <strong>%{count} conta</strong> a partir de <strong>%{filename}</strong> para suas <strong>listas</strong>. Novas listas serão criadas se não houver uma para a qual adicionar.
+        other: Você está prestes a adicionar até <strong>%{count} contas</strong> a partir de <strong>%{filename}</strong> para suas <strong>listas</strong>. Novas listas serão criadas se não houver uma para a qual adicionar.
       muting_html:
         one: Você está prestes a <strong>silenciar</strong> <strong>%{count}  conta</strong> de <strong>%{filename}</strong>.
         other: Você está prestes a <strong>silenciar</strong> mais de <strong>%{count} contas</strong> de <strong>%{filename}</strong>.
@@ -1824,6 +1906,8 @@ pt-BR:
       too_late: É tarde demais para solicitar uma revisão desta punição
   tags:
     does_not_match_previous_name: não corresponde ao nome anterior
+  terms_of_service:
+    title: Termos de Serviço
   themes:
     contrast: Mastodon  (Alto contraste)
     default: Mastodon (Noturno)
@@ -1855,6 +1939,10 @@ pt-BR:
     recovery_instructions_html: Se você perder acesso ao seu celular, você pode usar um dos códigos de recuperação abaixo para acessar a sua conta. <strong>Mantenha os códigos de recuperação em um local seguro</strong>. Por exemplo, você pode imprimi-los e guardá-los junto a outros documentos importantes.
     webauthn: Chaves de segurança
   user_mailer:
+    announcement_published:
+      description: 'Os administradores do %{domain} estão fazendo um anúncio:'
+      subject: Anúncio de serviço
+      title: Anúncio de serviço de %{domain}
     appeal_approved:
       action: Configurações da conta
       explanation: A revisão da punição na sua conta em %{strike_date} que você enviou em %{appeal_date} foi aprovada. Sua conta está novamente em situação regular.
@@ -1884,6 +1972,15 @@ pt-BR:
       further_actions_html: Se não foi você, recomendamos que você %{action} imediatamente e ative a autenticação de dois fatores para manter sua conta segura.
       subject: Sua conta foi acessada a partir de um novo endereço IP
       title: Um novo login
+    terms_of_service_changed:
+      agreement: Ao continuar a usar %{domain}, você concorda com estes termos. Se discordar dos termos atualizados, poderá encerrar seu acordo com %{domain} a qualquer momento excluindo sua conta.
+      changelog: 'Em resumo, veja o que essa atualização significa para você:'
+      description: 'Você recebeu este e-mail porque faremos algumas mudanças nos Termos de Serviço do %{domain}. Essas atualizações entrarão em vigor a partir de %{date}. Veja o que mudou aqui:'
+      description_html: Você recebeu este e-mail porque faremos algumas mudanças nos Termos de Serviço do %{domain}. Essas atualizações entrarão em vigor a partir de <strong>%{date}</strong>. <a href="%{path}" target="_blank">Veja o que mudou aqui</a>.
+      sign_off: A equipe do %{domain}
+      subject: Atualizações dos nossos termos de serviço
+      subtitle: Os termos de serviço do %{domain} estão mudando.
+      title: atualização importante
     warning:
       appeal: Enviar uma revisão
       appeal_description: Se você acredita que isso é um erro, você pode enviar uma revisão para a equipe de %{instance}.
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index 4015005b2d..ccd20e209d 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -309,6 +309,7 @@ pt-PT:
       title: Registo de auditoria
       unavailable_instance: "(nome de domínio indisponível)"
     announcements:
+      back: Voltar às mensagem de manutenção
       destroyed_msg: Mensagem de manutenção eliminada com sucesso!
       edit:
         title: Editar mensagem de manutenção
@@ -317,6 +318,9 @@ pt-PT:
       new:
         create: Criar mensagem de manutenção
         title: Nova mensagem de manutenção
+      preview:
+        explanation_html: 'O e-mail será enviado para <strong>%{display_count} utilizadores</strong>. O texto seguinte será incluído na mensagem de e-mail:'
+        title: Pré-visualização da mensagem de manutenção
       publish: Publicar
       published_msg: Mensagem de manutenção publicada com sucesso!
       scheduled_for: Agendado para %{time}
@@ -939,6 +943,7 @@ pt-PT:
         chance_to_review_html: "<strong>Os termos de serviço gerados não serão publicados automaticamente.</strong> Terá a possibilidade de rever os resultados. Por favor, preencha os detalhes necessários para prosseguir."
         explanation_html: O modelo de termos de serviço fornecido destina-se apenas a fins informativos e não deve ser interpretado como aconselhamento jurídico sobre qualquer assunto. Consulte o seu próprio consultor jurídico sobre a sua situação e questões jurídicas específicas que tenha.
         title: Configuração dos Termos de Serviço
+      going_live_on_html: Em vigor desde %{date}
       history: Histórico
       live: Em tempo real
       no_history: Ainda não há nenhuma alteração registada nos termos de serviço.
@@ -1904,6 +1909,10 @@ pt-PT:
     recovery_instructions_html: Se perderes o seu telemóvel, poderás usar um dos códigos de recuperação para voltares a ter acesso à tua conta. <strong>Guarda os códigos de recuperação em lugar seguro</strong>. Por exemplo, podes imprimi-los e guardá-los junto a outros documentos importantes.
     webauthn: Chaves de segurança
   user_mailer:
+    announcement_published:
+      description: 'Os administradores do %{domain} estão a fazer um anúncio:'
+      subject: Mensagem de manutenção do serviço
+      title: 'Mensagem de manutenção de %{domain} '
     appeal_approved:
       action: Configurações da conta
       explanation: A contestação à reprimenda contra a tua conta em %{strike_date}, enviada a %{appeal_date}, foi aceite. A tua conta encontra-se novamente em situação regular.
@@ -1936,8 +1945,8 @@ pt-PT:
     terms_of_service_changed:
       agreement: Ao continuar a utilizar %{domain}, concordas com estes termos. Se discordares dos termos atualizados, poderás rescindir o teu acordo com %{domain} a qualquer momento através da eliminação da tua conta.
       changelog: 'Em resumo, eis o que esta atualização significa para ti:'
-      description: 'Estás a receber esta mensagem de correio eletrónico porque estamos a fazer algumas alterações aos nossos termos de serviço em %{domain}. Recomendamos que revejas os termos atualizados na íntegra aqui:'
-      description_html: Estás a receber esta mensagem de correio eletrónico porque estamos a fazer algumas alterações aos nossos termos de serviço em %{domain}. Recomendamos que revejas os <a href="%{path}" target="_blank">termos atualizados na íntegra aqui</a>.
+      description: 'Está a receber esta mensagem porque vamos alterar os nossos termos de serviço em %{domain}. Estas atualizações entram em vigor em %{date}. Recomendamos que leia os termos de serviço atualizados em:'
+      description_html: Está a receber esta mensagem porque vamos alterar os nossos termos de serviço em %{domain}. Estas atualizações entram em vigor em <strong>%{date}</strong>. Recomendamos que leia os <a href="%{path}" target="_blank">termos de serviço atualizados aqui</a>.
       sign_off: A equipa de %{domain}
       subject: Atualizações dos nossos termos de serviço
       subtitle: Os termos de serviço de %{domain} estão a mudar
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index 37c51cf1b5..aeaa1e283c 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -23,6 +23,7 @@ ro:
       one: Postare
       other: De Postări
     posts_tab_heading: Postări
+    self_follow_error: Urmărirea propriului cont nu este permisă
   admin:
     account_actions:
       action: Efectuează acțiunea
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 2db629cafc..1f01f622f5 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -290,7 +290,7 @@ ru:
         reject_user_html: "%{name} отклонил(а) регистрацию %{target}"
         remove_avatar_user_html: "%{name} убрал(а) аватарку пользователя %{target}"
         reopen_report_html: "%{name} повторно открыл(а) жалобу %{target}"
-        resend_user_html: "%{name} повторно отправил письмо с подтверждением для %{target}"
+        resend_user_html: "%{name} повторно отправил(а) письмо с подтверждением для %{target}"
         reset_password_user_html: "%{name} сбросил(а) пароль пользователя %{target}"
         resolve_report_html: "%{name} решил(а) жалобу %{target}"
         sensitive_account_html: "%{name} установил(а) отметку файлов %{target} как «деликатного характера»"
@@ -315,6 +315,7 @@ ru:
       title: Журнал аудита
       unavailable_instance: "(доменное имя недоступно)"
     announcements:
+      back: Вернуться к объявлениям
       destroyed_msg: Объявление удалено.
       edit:
         title: Редактировать объявление
@@ -323,6 +324,9 @@ ru:
       new:
         create: Создать объявление
         title: Новое объявление
+      preview:
+        explanation_html: 'Сообщение будет отравлено <strong>%{display_count} пользователям</strong>. В теле письма будет указан следующий текст:'
+        title: Предпросмотр объявления по электронной почте
       publish: Опубликовать
       published_msg: Объявление опубликовано.
       scheduled_for: Запланировано на %{time}
@@ -471,7 +475,7 @@ ru:
         create: Создать блокировку
         resolve: Проверить домен
         title: Блокировка нового почтового домена
-      no_email_domain_block_selected: Блоки почтовых доменов не были изменены, так как ни один из них не был выбран
+      no_email_domain_block_selected: Блокировки почтовых доменов не были изменены, так как ни один из них не был выбран
       not_permitted: Не разрешено
       resolved_dns_records_hint_html: Доменное имя указывает на следующие MX-домены, которые в конечном итоге отвечают за прием электронной почты. Блокировка MX-домена будет блокировать регистрации с любого адреса электронной почты, который использует тот же MX-домен, даже если видимое доменное имя отличается от него. <strong>Будьте осторожны, чтобы не заблокировать основных провайдеров электронной почты</strong>
       resolved_through_html: Разрешено через %{domain}
@@ -491,6 +495,10 @@ ru:
       new:
         title: Импорт доменных блокировок
       no_file: Файл не выбран
+    fasp:
+      providers:
+        sign_in:
+        status: Пост
     follow_recommendations:
       description_html: "<strong>Следуйте рекомендациям, чтобы помочь новым пользователям быстро находить интересный контент</strong>. Если пользователь не взаимодействовал с другими в достаточной степени, чтобы сформировать персонализированные рекомендации, вместо этого рекомендуется использовать эти учетные записи. Они пересчитываются на ежедневной основе на основе комбинации аккаунтов с наибольшим количеством недавних взаимодействий и наибольшим количеством местных подписчиков для данного языка."
       language: Для языка
@@ -639,7 +647,7 @@ ru:
         suspend_description_html: Аккаунт и все его содержимое будут недоступны и в конечном итоге удалены, и взаимодействие с ним будет невозможно. Это действие можно отменить в течение 30 дней. Отменяет все жалобы против этого аккаунта.
       actions_description_html: Выберите действие, чтобы разрешить данную жалобу. Если вы примете меры модерации против аккаунта, его владелец получит уведомление по электронной почте, кроме тех случаев, когда выбрана категория <strong>Спам</strong>.
       actions_description_remote_html: Решите вопрос о том, какие меры необходимо принять для урегулирования этой жалобы. Это повлияет только на то, как <strong>ваш</strong> сервер взаимодействует с этим удаленным аккаунтом и обрабатывает его содержимое.
-      actions_no_posts: У этого отчета нет связанных с ним сообщений для удаления
+      actions_no_posts: У этой жалобы нет связанных с ней постов для удаления
       add_to_report: Прикрепить ещё
       already_suspended_badges:
         local: На этом сервере уже забанен
@@ -680,7 +688,7 @@ ru:
       report: Жалоба №%{id}
       reported_account: Учётная запись нарушителя
       reported_by: Отправитель жалобы
-      reported_with_application: Сообщается с приложением
+      reported_with_application: Использованное для отправки жалобы приложение
       resolved: Решённые
       resolved_msg: Жалоба обработана, спасибо!
       skip_to_actions: Перейти к действиям
@@ -828,7 +836,7 @@ ru:
           approved: Для регистрации требуется подтверждение
           none: Никто не может регистрироваться
           open: Все могут регистрироваться
-        warning_hint: Мы рекомендуем использовать "Требуется одобрение для регистрации", если вы не уверены, что ваша команда модераторов сможет своевременно справиться со спамом и вредоносными регистрациями.
+        warning_hint: Мы рекомендуем использовать "Требуется одобрение для регистрации", если вы не уверены, что ваша команда модераторов сможет своевременно справиться со спамом и злоумышленными регистрациями.
       security:
         authorized_fetch: Требовать аутентификацию от федеративных серверов
         authorized_fetch_hint: Требование аутентификации от федеративных серверов позволяет более строго соблюдать блокировки как на уровне пользователя, так и на уровне сервера. Однако при этом снижается производительность, уменьшается охват ваших ответов и могут возникнуть проблемы совместимости с некоторыми федеративными сервисами. Кроме того, это не помешает специальным исполнителям получать ваши публичные сообщения и учётные записи.
@@ -938,19 +946,19 @@ ru:
     tags:
       moderation:
         not_trendable: Не в тренде
-        not_usable: Невозможно использовать
-        pending_review: В ожидании обзора
-        review_requested: Обзор запрошен
-        reviewed: Рассмотрено
+        not_usable: Недоступный
+        pending_review: На рассмотрении
+        review_requested: Запрошено рассмотрение
+        reviewed: Рассмотренный
         title: Статус
-        trendable: Трендовый
-        unreviewed: Без рецензии
-        usable: Полезное
+        trendable: В тренде
+        unreviewed: Нерассмотренный
+        usable: Доступный
       name: Название
-      newest: Новейший
-      oldest: Старейший
+      newest: Сначала новые
+      oldest: Сначала старые
       open: Посмотреть публично
-      reset: Сброс
+      reset: Сбросить
       review: Состояние проверки
       search: Поиск
       title: Хэштеги
@@ -967,6 +975,7 @@ ru:
         chance_to_review_html: "<strong>Сгенерированное пользовательское соглашение не будет опубликовано автоматически.</strong> У вас будет возможность просмотреть результат. Введите все необходимые сведения, чтобы продолжить."
         explanation_html: Шаблон пользовательского соглашения приводится исключительно в ознакомительных целях, и не может рассматриваться как юридическая консультация по тому или иному вопросу. Обратитесь к своему юрисконсульту насчёт вашей ситуации и имеющихся правовых вопросов.
         title: Создание пользовательского соглашения
+      going_live_on_html: Вступило в силу с %{date}
       history: История
       live: Действует
       no_history: Нет зафиксированных изменений пользовательского соглашения.
@@ -999,7 +1008,7 @@ ru:
         confirm_allow: Вы уверены, что хотите разрешить выбранные ссылки?
         confirm_allow_provider: Вы уверены, что хотите разрешить выбранных провайдеров?
         confirm_disallow: Вы уверены, что хотите запретить выбранные ссылки?
-        confirm_disallow_provider: Вы уверены, что хотите запретить выбранных поставщиков?
+        confirm_disallow_provider: Вы уверены, что хотите запретить выбранных провайдеров?
         description_html: Это ссылки, которыми в настоящее время много пользуются аккаунты, с которых ваш сервер видит сообщения. Это может помочь вашим пользователям узнать, что происходит в мире. Никакие ссылки не отображаются публично, пока вы не одобрите издателя. Вы также можете разрешить или отклонить индивидуальные ссылки.
         disallow: Запретить ссылку
         disallow_provider: Отклонить издание
@@ -1065,14 +1074,14 @@ ru:
           many: За последнюю неделю использовало %{count} человек
           one: За последнюю неделю использовал один человек
           other: За последнюю неделю использовал %{count} человек
-      title: Рекомендации и тенденции
+      title: Рекомендации и тренды
       trending: Популярное
     warning_presets:
       add_new: Добавить
       delete: Удалить
       edit_preset: Удалить шаблон предупреждения
       empty: Вы еще не определили пресеты предупреждений.
-      title: Предупреждающие пред установки
+      title: Шаблоны предупреждений
     webhooks:
       add_new: Добавить конечную точку
       delete: Удалить
@@ -1247,6 +1256,7 @@ ru:
     too_fast: Форма отправлена слишком быстро, попробуйте еще раз.
     use_security_key: Использовать ключ безопасности
     user_agreement_html: Мной прочитаны и приняты<a href="%{terms_of_service_path}" target="_blank">пользовательское соглашение</a> и <a href="%{privacy_policy_path}" target="_blank">политика конфиденциальности</a>
+    user_privacy_agreement_html: Мной прочитана и принята <a href="%{privacy_policy_path}" target="_blank">политика конфиденциальности</a>
   author_attribution:
     example_title: Образец текста
     hint_html: Публикуете ли вы свои статьи где-либо ещё кроме Mastodon? Если да, то ваше авторство может быть упомянуто, когда ими делятся в Mastodon.
@@ -1866,7 +1876,7 @@ ru:
       domain_block: Приостановка сервера (%{target_name})
       user_domain_block: Вы заблокировали %{target_name}
     lost_followers: Потерянные подписчики
-    lost_follows: Потерянный следует
+    lost_follows: Потерянные подписки
     preamble: Вы можете потерять подписчиков и последователей, если заблокируете домен или, если ваши модераторы решат приостановить работу удаленного сервера. Когда это произойдет, вы сможете загрузить списки разорванных отношений, чтобы проверить их и, возможно, импортировать на другой сервер.
     purged: Информация об этом сервере была удалена администраторами вашего сервера.
     type: Событие
@@ -1989,6 +1999,10 @@ ru:
     recovery_instructions_html: 'Пожалуйста, сохраните коды ниже в надёжном месте: они понадобятся, чтобы войти в учётную запись, если вы потеряете доступ к своему смартфону. Вы можете вручную переписать их, распечатать и спрятать среди важных документов или, например, в любимой книжке. <strong>Каждый код действителен только один раз</strong>.'
     webauthn: Ключи безопасности
   user_mailer:
+    announcement_published:
+      description: 'Администраторы %{domain} опубликовали новое объявление:'
+      subject: Сервисное объявление
+      title: Сервисное объявление %{domain}
     appeal_approved:
       action: Настройки аккаунта
       explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись снова на хорошем счету.
@@ -2021,8 +2035,8 @@ ru:
     terms_of_service_changed:
       agreement: Продолжая использовать %{domain}, вы соглашаетесь с этими условиями. Если вы не согласны с новыми условиями, вы в любой момент можете удалить вашу учётную запись на %{domain}.
       changelog: 'Вот что обновление условий будет значит для вас в общих чертах:'
-      description: 'Вы получили это сообщение, потому что мы внесли некоторые изменения в пользовательское соглашение %{domain}. Рекомендуем вам ознакомиться с обновлёнными условиями по ссылке:'
-      description_html: Вы получили это сообщение, потому что мы внесли некоторые изменения в пользовательское соглашение %{domain}. Рекомендуем вам ознакомиться с <a href="%{path}" target="_blank">обновлёнными условиями</a>.
+      description: 'Вы получили это сообщение, потому что мы внесли некоторые изменения в пользовательское соглашение %{domain}. Эти изменения вступят в силу %{date}. Рекомендуем вам ознакомиться с обновлёнными условиями по ссылке:'
+      description_html: Вы получили это сообщение, потому что мы внесли некоторые изменения в пользовательское соглашение %{domain}. Эти изменения вступят в силу <strong>%{date}</strong>. Рекомендуем вам ознакомиться с <a href="%{path}" target="_blank">обновлёнными условиями</a>.
       sign_off: Ваш %{domain}
       subject: Обновления наших условий использования
       subtitle: На %{domain} изменилось пользовательское соглашение
diff --git a/config/locales/simple_form.ast.yml b/config/locales/simple_form.ast.yml
index 997423449d..9e5db2ba30 100644
--- a/config/locales/simple_form.ast.yml
+++ b/config/locales/simple_form.ast.yml
@@ -104,11 +104,11 @@ ast:
         setting_aggregate_reblogs: Agrupar los artículos compartíos nes llinies de tiempu
         setting_always_send_emails: Unviar siempre los avisos per corréu electrónicu
         setting_auto_play_gif: Reproducir automáticamente los GIFs
-        setting_boost_modal: Amosar el diálogu de confirmación enantes de compartir un artículu
+        setting_boost_modal: Amosar el diálogu de confirmación enantes de compartir una publicación
         setting_default_language: Llingua de los artículos
         setting_default_privacy: Privacidá de los artículos
         setting_default_sensitive: Marcar siempre tol conteníu como sensible
-        setting_delete_modal: Amosar el diálogu de confirmación enantes de desaniciar un artículu
+        setting_delete_modal: Amosar el diálogu de confirmación enantes de desaniciar una publicación
         setting_disable_hover_cards: Desactivar la previsualización de perfiles al pasar el mur penriba
         setting_disable_swiping: Desactivar el movimientu de desplazamientu
         setting_display_media: Conteníu multimedia
@@ -160,12 +160,12 @@ ast:
           sign_up_block: Bloquiar el rexistru de cuentes nueves
           sign_up_requires_approval: Llendar les cuentes rexistraes nueves
       notification_emails:
-        favourite: Daquién marcó como favoritu'l to artículu
+        favourite: Daquién marcó como favorita la to publicación
         follow: Daquién te sigue
         follow_request: Daquién solicitó siguite
         mention: Daquién te mentó
         pending_account: Una cuenta nueva precisa una revisión
-        reblog: Daquién compartió'l to artículu
+        reblog: Daquién compartió la to publicación
         report: Unvióse un informe nuevu
         software_updates:
           label: Hai disponible un anovamientu de Mastodon
diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml
index a1d0137b13..6180f51dd3 100644
--- a/config/locales/simple_form.bg.yml
+++ b/config/locales/simple_form.bg.yml
@@ -75,6 +75,7 @@ bg:
       filters:
         action: Изберете кое действие да се извърши, прецеждайки съвпаденията на публикацията
         actions:
+          blur: Скриване на мултимедия зад предупреждение, но без скриване на самия текст
           hide: Напълно скриване на филтрираното съдържание, сякаш не съществува
           warn: Скриване на филтрираното съдържание зад предупреждение, споменавайки заглавието на филтъра
       form_admin_settings:
@@ -88,6 +89,7 @@ bg:
         favicon: WEBP, PNG, GIF или JPG. Заменя стандартната сайтоикона на Mastodon с произволна икона.
         mascot: Замества илюстрацията в разширения уеб интерфейс.
         media_cache_retention_period: Мултимедийни файлове от публикации, направени от отдалечени потребители, се сринаха в сървъра ви. Задавайки положителна стойност, мултимедията ще се изтрие след посочения брой дни. Ако се искат мултимедийни данни след изтриването, то ще се изтегли пак, ако още е наличен източникът на съдържание. Поради ограниченията за това колко често картите за предварващ преглед на връзките анкетират сайтове на трети страни, се препоръчва да зададете тази стойност на поне 14 дни или картите за предварващ преглед на връзките няма да се обновяват при поискване преди този момент.
+        min_age: Потребителите ще се питат да потвърдят рождената си дата по време на регириране
         peers_api_enabled: Списък от имена на домейни, с които сървърът се е свързал във федивселената. Тук не се включват данни за това дали федерирате с даден сървър, а само за това дали сървърът ви знае за него. Това се ползва от услуги, събиращи статистика за федерацията в общия смисъл.
         profile_directory: Указателят на профили вписва всички потребители, избрали да бъдат откриваеми.
         require_invite_text: Когато регистрацията изисква ръчно одобрение, то направете текстовото поле за това "Защо желаете да се присъедините?" по-скоро задължително, отколкото по желание
@@ -132,13 +134,20 @@ bg:
         name: Можете да смените само употребата на големи/малки букви, например, за да е по-четимо
       terms_of_service:
         changelog: Може да се структурира със синтаксиса на Markdown.
+        effective_date: Разумната времева рамка може да обхваща някъде от 10 до 30 дни от датата, на която сте известили потребителите си.
         text: Може да се структурира със синтаксиса на Markdown.
       terms_of_service_generator:
         admin_email: Правните бележки включват насрещни известия, постановления на съда, заявки за сваляне и заявки от правоохранителните органи.
+        arbitration_address: Може да е същото като физическия адрес горе или "неприложимо", ако се употребява имейл.
+        arbitration_website: Може да е уеб формуляр или "неналично", ако употребявате имейл.
+        choice_of_law: Град, регион, територия, щат или държава, чиито вътрешни материални права ще уреждат всички искове.
+        dmca_email: Може да е същият имейл, използван за "Имейл адрес за правни известия" по-горе.
         domain: Неповторимо идентифициране на онлайн услугата, която предоставяте.
         jurisdiction: Впишете държавата, където живее всеки, който плаща сметките. Ако е дружество или друго образувание, то впишете държавата, в която е регистрирано, и градът, регионът, територията или щатът според случая.
+        min_age: Не трябва да е под изискваната минимална възраст от закона на юрисдикцията ви.
       user:
         chosen_languages: Само публикации на отметнатите езици ще се показват в публичните часови оси
+        date_of_birth: Трябва да се уверим, че сте поне на %{age}, за да употребявате Mastodon. Няма да съхраняваме това.
         role: Ролята управлява какви позволения има потребителят.
       user_role:
         color: Цветът, използван за ролите в потребителския интерфейс, като RGB в шестнадесетичен формат
@@ -229,6 +238,7 @@ bg:
         setting_display_media_show_all: Показване на всичко
         setting_expand_spoilers: Винаги разширяване на публикации, отбелязани с предупреждения за съдържание
         setting_hide_network: Скриване на социалния ви свързан граф
+        setting_missing_alt_text_modal: Показване на диалогов прозорец потвърждение преди публикуване на мултимедия без алт. текст
         setting_reduce_motion: Обездвижване на анимациите
         setting_system_font_ui: Употреба на стандартния шрифт на системата
         setting_system_scrollbars_ui: Употреба на системната подразбираща се лента за превъртане
@@ -250,6 +260,7 @@ bg:
         name: Хаштаг
       filters:
         actions:
+          blur: Скриване на мултимедията с предупреждение
           hide: Напълно скриване
           warn: Скриване зад предупреждение
       form_admin_settings:
@@ -263,6 +274,7 @@ bg:
         favicon: Сайтоикона
         mascot: Плашило талисман по избор (остаряло)
         media_cache_retention_period: Период на запазване на мултимедийния кеш
+        min_age: Минимално възрастово изискване
         peers_api_enabled: Публикуване на списъка с открити сървъри в API
         profile_directory: Показване на директорията от профили
         registrations_mode: Кой може да се регистрира
@@ -328,16 +340,22 @@ bg:
         usable: Позволяване на публикациите да употребяват този хаштаг локално
       terms_of_service:
         changelog: Какво е променено?
+        effective_date: Дата на влизане в сила
         text: Условия на услугата
       terms_of_service_generator:
         admin_email: Имейл адрес за правни известия
         arbitration_address: Физичен адрес за арбитражни известия
         arbitration_website: Уебсайт за подаване на арбитражни известия
+        choice_of_law: Избор на закон
         dmca_address: Физичен адрес за DMCA/бележки за авторски права
         dmca_email: Адрес на е-поща за DMCA/бележки за авторски права
         domain: Домейн
         jurisdiction: Законова юрисдикция
+        min_age: Минимална възраст
       user:
+        date_of_birth_1i: Ден
+        date_of_birth_2i: Месец
+        date_of_birth_3i: Година
         role: Роля
         time_zone: Часова зона
       user_role:
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index 0abf5b1331..2f85738c32 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -75,6 +75,7 @@ ca:
       filters:
         action: Tria quina acció cal executar quan un apunt coincideixi amb el filtre
         actions:
+          blur: Amaga el contingut multimèdia rere un avís, sense amagar el text en si
           hide: Ocultar completament el contingut filtrat, comportant-se com si no existís
           warn: Oculta el contingut filtrat darrere d'un avís mencionant el títol del filtre
       form_admin_settings:
@@ -88,6 +89,7 @@ ca:
         favicon: WEBP, PNG, GIF o JPG. Canvia la icona per defecte de Mastodon a la pestanya del navegador per una de personalitzada.
         mascot: Anul·la la il·lustració en la interfície web avançada.
         media_cache_retention_period: El vostre servidor conserva una còpia dels fitxers multimèdia de les publicacions dels usuaris remots. Si s'indica un valor positiu, s'esborraran passats els dies indicats. Si el fitxer es torna a demanar un cop esborrat, es tornarà a baixar si el contingut origen segueix disponible. Per causa de les restriccions en la freqüència amb què es poden demanar les targetes de previsualització d'altres servidors, es recomana definir aquest valor com a mínim a 14 dies, o les targetes de previsualització no s'actualizaran a demanda abans d'aquest termini.
+        min_age: Es demanarà als usuaris la data de naixement durant la inscripció
         peers_api_enabled: Una llista de noms de domini que aquest servidor ha trobat al fedivers. No inclou cap dada sobre si estàs federat amb un servidor determinat, només si el teu en sap res. La fan servir, en un sentit general, serveis que recol·lecten estadístiques sobre la federació.
         profile_directory: El directori de perfils llista tots els usuaris que tenen activat ser descoberts.
         require_invite_text: Quan el registre requereixi aprovació manual, fes que sigui obligatori en lloc d'opcional d'escriure el text de la sol·licitud d'invitació "Per què vols unir-te?"
@@ -130,11 +132,20 @@ ca:
         show_application: Sempre podràs veure quina aplicació ha publicat els teus tuts.
       tag:
         name: Només pots canviar la caixa de les lletres, per exemple, per fer-la més llegible
+      terms_of_service:
+        changelog: S'hi pot fer servir sintaxi Markdown.
+        effective_date: Un termini raonable podria ser d'entre 10 i 30 dies des que notifiques els usuaris.
+        text: S'hi pot fer servir sintaxi Markdown.
       terms_of_service_generator:
+        admin_email: Els avisos legals inclouen contraavisos, ordres judicials, sol·licituds de retirada i sol·licituds d'aplicació de la llei.
+        choice_of_law: Ciutat, regió, territori o estat els jutjats del qual hauran de dirimir qualsevol disputa legal.
+        dmca_email: Pot ser la mateixa que heu fet servir abans per a «adreça-e per als avisos legals».
         domain: Identificació única del servei en línia que oferiu.
         jurisdiction: Indiqueu el país on resideix qui paga les factures. Si és una empresa o una altra entitat, indiqueu el país en què està registrada, així com la ciutat, regió, territori o estat, segons calqui.
+        min_age: No hauria de ser inferior a l'edat mínima exigida per la llei de la vostra jurisdicció.
       user:
         chosen_languages: Quan estigui marcat, només es mostraran els tuts de les llengües seleccionades en les línies de temps públiques
+        date_of_birth: Ens hem d'assegurar que teniu %{age} anys com a mínim. No desarem aquesta dada.
         role: El rol controla quins permisos té l'usuari.
       user_role:
         color: Color que s'usarà per al rol a tota la interfície d'usuari, com a RGB en format hexadecimal
@@ -247,6 +258,7 @@ ca:
         name: Etiqueta
       filters:
         actions:
+          blur: Amaga el contingut multimèdia amb un avís
           hide: Oculta completament
           warn: Oculta amb un avís
       form_admin_settings:
@@ -260,6 +272,7 @@ ca:
         favicon: Icona de preferits
         mascot: Mascota personalitzada (llegat)
         media_cache_retention_period: Període de retenció del cau multimèdia
+        min_age: Edat mínima requerida
         peers_api_enabled: Publica a l'API la llista de servidors descoberts
         profile_directory: Habilita el directori de perfils
         registrations_mode: Qui es pot registrar
@@ -325,16 +338,22 @@ ca:
         usable: Permet a les publicacions emprar aquesta etiqueta localment
       terms_of_service:
         changelog: Què ha canviat?
+        effective_date: Data efectiva
         text: Condicions de servei
       terms_of_service_generator:
-        admin_email: Adreça-e per a les notificacions legals
+        admin_email: Adreça-e per als avisos legals
         arbitration_address: Adreça física per a les notificacions d'arbitratge
         arbitration_website: Lloc web per a enviar les notificacions d'arbitratge
+        choice_of_law: Legislació aplicable
         dmca_address: Adreça física per a les notificacions de copyright/DMCA
         dmca_email: Adreça-e per a les notificacions de copyright/DMCA
         domain: Domini
         jurisdiction: Jurisdicció
+        min_age: Edat mínima
       user:
+        date_of_birth_1i: Dia
+        date_of_birth_2i: Mes
+        date_of_birth_3i: Any
         role: Rol
         time_zone: Zona horària
       user_role:
diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml
index 37ad72a7e4..00abf91d3e 100644
--- a/config/locales/simple_form.cs.yml
+++ b/config/locales/simple_form.cs.yml
@@ -3,7 +3,7 @@ cs:
   simple_form:
     hints:
       account:
-        attribution_domains: Jeden na řádek. Chrání před falešným připisování autorství.
+        attribution_domains: Jeden na řádek. Chrání před falešným připisováním autorství.
         discoverable: Vaše veřejné příspěvky a profil mohou být zobrazeny nebo doporučeny v různých oblastech Mastodonu a váš profil může být navrhován ostatním uživatelům.
         display_name: Vaše celé jméno nebo přezdívka.
         fields: Vaše domovská stránka, zájmena, věk, cokoliv chcete.
@@ -75,6 +75,7 @@ cs:
       filters:
         action: Vyberte, jakou akci provést, když příspěvek odpovídá filtru
         actions:
+          blur: Skrýt média za varováním, bez skrytí samotného textu
           hide: Úplně schovat filtrovaný obsah tak, jako by neexistoval
           warn: Schovat filtrovaný obsah za varováním zmiňujicím název filtru
       form_admin_settings:
@@ -88,6 +89,7 @@ cs:
         favicon: WEBP, PNG, GIF nebo JPG. Nahradí výchozí favicon Mastodonu vlastní ikonou.
         mascot: Přepíše ilustraci v pokročilém webovém rozhraní.
         media_cache_retention_period: Mediální soubory z příspěvků vzdálených uživatelů se ukládají do mezipaměti na vašem serveru. Pokud je nastaveno na kladnou hodnotu, budou média po zadaném počtu dní odstraněna. Pokud jsou mediální data vyžádána po jejich odstranění, budou znovu stažena, pokud je zdrojový obsah stále k dispozici. Vzhledem k omezením týkajícím se četnosti dotazů karet náhledů odkazů na weby třetích stran se doporučuje nastavit tuto hodnotu alespoň na 14 dní, jinak nebudou karty náhledů odkazů na vyžádání aktualizovány dříve.
+        min_age: Uživatelé budou požádáni, aby při registraci potvrdili datum svého narození
         peers_api_enabled: Seznam názvů domén se kterými se tento server setkal ve fediversu. Neobsahuje žádná data o tom, zda jste federovali s daným serverem, pouze že o něm váš server ví. Toto je využíváno službami, které sbírají o federování statistiku v obecném smyslu.
         profile_directory: Adresář profilu obsahuje seznam všech uživatelů, kteří se přihlásili, aby mohli být nalezeni.
         require_invite_text: Pokud přihlášení vyžaduje ruční schválení, měl by být textový vstup „Proč se chcete připojit?“ povinný spíše než volitelný
@@ -132,17 +134,21 @@ cs:
         name: Můžete měnit pouze velikost písmen, například kvůli lepší čitelnosti
       terms_of_service:
         changelog: Může být strukturováno pomocí Markdown syntaxu.
+        effective_date: Rozumné časové rozpětí může být kdykoliv od 10 do 30 dnů od doby, kdy upozorníte své uživatele.
         text: Může být strukturováno pomocí Markdown syntaxu.
       terms_of_service_generator:
         admin_email: Právní oznámení zahrnují protioznámení, soudní příkazy, žádosti o stáhnutí příspěvků a žádosti od právních vymahačů.
-        arbitration_address: Může být stejné jako výše uvedená fyzická adresa, nebo „N/A“, pokud používáte e-mail
-        arbitration_website: Může být webový formulář nebo „N/A“, pokud používáte e-mail
-        dmca_address: V případě provozovatelů v USA použijte adresu zapsanou v DMCA Designated Agent Directory. Na přímou žádost je možné použít namísto domovské adresy PO box, použijte DMCA Designated Agent Post Office Box Waiver Request k e-mailovaní Copyright Office a řekněte jim, že jste malým moderátorem obsahu, který se obavá pomsty nabo odplaty za Vaši práci a potřebujete použít PO box, abyste schovali Vaši domovskou adresu před širokou veřejností.
-        dmca_email: Může být stejný e-mail použitý pro „E-mail adresy pro právní upozornění“
+        arbitration_address: Může být stejné jako výše uvedená fyzická adresa, nebo „N/A“, pokud používáte e-mail.
+        arbitration_website: Může být webový formulář nebo „N/A“, pokud používáte email.
+        choice_of_law: Město, region, území nebo stát, jehož vnitřní hmotné právo stanovuje povinnosti pro veškeré nároky.
+        dmca_address: V případě provozovatelů v USA použijte adresu zapsanou v DMCA Designated Agent Directory. Na přímou žádost je možné použít namísto domovské adresy PO box. V tomto případě použijte DMCA Designated Agent Post Office Box Waiver Request k zaslání emailu Copyright Office a řekněte jim, že jste malým moderátorem obsahu, který se obavá pomsty nabo odplaty za Vaši práci a potřebujete použít PO box, abyste schovali Vaši domovskou adresu před širokou veřejností.
+        dmca_email: Může být stejný e-mail použitý pro „E-mailová adresa pro právní upozornění.“
         domain: Jedinečná identifikace online služby, kterou poskytujete.
         jurisdiction: Uveďte zemi, kde žije ten, kdo platí účty. Pokud je to společnost nebo jiný subjekt, uveďte zemi, v níž je zapsán do obchodního rejstříku, a město, region, území, případně stát, pokud je to povinné.
+        min_age: Neměla by být pod minimálním věkem požadovaným zákony vaší jurisdikce.
       user:
         chosen_languages: Po zaškrtnutí budou ve veřejných časových osách zobrazeny pouze příspěvky ve zvolených jazycích
+        date_of_birth: Musíme se ujistit, že je Vám alespoň %{age}, abyste mohli používat Mastodon. Nebudeme to ukládat.
         role: Role určuje, která oprávnění uživatel má.
       user_role:
         color: Barva, která má být použita pro roli v celém UI, jako RGB v hex formátu
@@ -233,6 +239,7 @@ cs:
         setting_display_media_show_all: Zobrazit vše
         setting_expand_spoilers: Vždy rozbalit příspěvky označené varováními o obsahu
         setting_hide_network: Skrýt mou síť
+        setting_missing_alt_text_modal: Zobrazit potvrzovací dialog před odesláním médií bez alt textu
         setting_reduce_motion: Omezit pohyb v animacích
         setting_system_font_ui: Použít výchozí písmo systému
         setting_system_scrollbars_ui: Použít výchozí posuvník systému
@@ -254,6 +261,7 @@ cs:
         name: Hashtag
       filters:
         actions:
+          blur: Skrýt média za varováním
           hide: Zcela skrýt
           warn: Skrýt s varováním
       form_admin_settings:
@@ -267,6 +275,7 @@ cs:
         favicon: Favicon
         mascot: Vlastní maskot (zastaralé)
         media_cache_retention_period: Doba uchovávání mezipaměti médií
+        min_age: Minimální věková hranice
         peers_api_enabled: Zveřejnit seznam nalezených serverů v API
         profile_directory: Povolit adresář profilů
         registrations_mode: Kdo se může přihlásit
@@ -332,16 +341,22 @@ cs:
         usable: Povolit příspěvkům používat tento hashtag lokálně
       terms_of_service:
         changelog: Co se změnilo?
+        effective_date: Datum platnosti
         text: Podmínky užití
       terms_of_service_generator:
         admin_email: E-mailová adresa pro právní upozornění
         arbitration_address: Fyzická adresa pro upozornění na arbitrační řízení
         arbitration_website: Webová stránka pro zasílání arbitračních upozornění
+        choice_of_law: Volba zákona
         dmca_address: Fyzická adresa pro oznámení DMCA/porušení autorských práv
         dmca_email: E-mailová adresa pro oznámení DMCA/porušení autorských práv
         domain: Doména
         jurisdiction: Právní příslušnost
+        min_age: Věková hranice
       user:
+        date_of_birth_1i: Den
+        date_of_birth_2i: Měsíc
+        date_of_birth_3i: Rok
         role: Role
         time_zone: Časové pásmo
       user_role:
diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml
index 1220ceabf3..c9c7862a91 100644
--- a/config/locales/simple_form.cy.yml
+++ b/config/locales/simple_form.cy.yml
@@ -3,6 +3,7 @@ cy:
   simple_form:
     hints:
       account:
+        attribution_domains: Un i bob llinell. Yn diogelu rhag priodoli ffug.
         discoverable: Mae'n bosibl y bydd eich postiadau cyhoeddus a'ch proffil yn cael sylw neu'n cael eu hargymell mewn gwahanol feysydd o Mastodon ac efallai y bydd eich proffil yn cael ei awgrymu i ddefnyddwyr eraill.
         display_name: Eich enw llawn neu'ch enw hwyl.
         fields: Eich tudalen cartref, rhagenwau, oed, neu unrhyw beth.
@@ -40,7 +41,7 @@ cy:
       defaults:
         autofollow: Bydd pobl sy'n cofrestru drwy'r gwahoddiad yn eich dilyn yn awtomatig
         avatar: WEBP, PNG, GIF neu JPG. %{size} ar y mwyaf. Bydd yn cael ei leihau i %{dimensions}px
-        bot: Mae'r cyfrif hwn yn perfformio gweithredoedd awtomatig yn bennaf ac mae'n bosib nad yw'n cael ei fonitro
+        bot: Rhoi gwybod i eraill bod y cyfrif hwn yn perfformio gweithredoedd awtomatig yn bennaf ac mae'n bosib nad yw'n cael ei fonitro
         context: Un neu fwy cyd-destun lle dylai'r hidlydd weithio
         current_password: At ddibenion diogelwch, nodwch gyfrinair y cyfrif cyfredol
         current_username: I gadarnhau, nodwch enw defnyddiwr y cyfrif cyfredol
@@ -74,6 +75,7 @@ cy:
       filters:
         action: Dewiswch pa weithred i'w chyflawni pan fydd postiad yn cyfateb i'r hidlydd
         actions:
+          blur: Cuddio cyfryngau tu ôl i rybudd, heb guddio'r testun ei hun
           hide: Cuddiwch y cynnwys wedi'i hidlo'n llwyr, gan ymddwyn fel pe na bai'n bodoli
           warn: Cuddiwch y cynnwys wedi'i hidlo y tu ôl i rybudd sy'n sôn am deitl yr hidlydd
       form_admin_settings:
@@ -87,6 +89,7 @@ cy:
         favicon: WEBP, PNG, GIF neu JPG. Yn diystyru'r favicon Mastodon rhagosodedig gydag eicon cyfaddas.
         mascot: Yn diystyru'r darlun yn y rhyngwyneb gwe uwch.
         media_cache_retention_period: Mae ffeiliau cyfryngau o bostiadau a wneir gan ddefnyddwyr o bell yn cael eu storio ar eich gweinydd. Pan gaiff ei osod i werth positif, bydd y cyfryngau yn cael eu dileu ar ôl y nifer penodedig o ddyddiau. Os gofynnir am y data cyfryngau ar ôl iddo gael ei ddileu, caiff ei ail-lwytho i lawr, os yw'r cynnwys ffynhonnell yn dal i fod ar gael. Oherwydd cyfyngiadau ar ba mor aml y mae cardiau rhagolwg cyswllt yn pleidleisio i wefannau trydydd parti, argymhellir gosod y gwerth hwn i o leiaf 14 diwrnod, neu ni fydd cardiau rhagolwg cyswllt yn cael eu diweddaru ar alw cyn yr amser hwnnw.
+        min_age: Mae gofyn i ddefnyddwyr gadarnhau eu dyddiad geni wrth gofrestru
         peers_api_enabled: Rhestr o enwau parth y mae'r gweinydd hwn wedi dod ar eu traws yn y ffediws. Nid oes unrhyw ddata wedi'i gynnwys yma ynghylch a ydych chi'n ffedereiddio â gweinydd penodol, dim ond bod eich gweinydd yn gwybod amdano. Defnyddir hwn gan wasanaethau sy'n casglu ystadegau ar ffedereiddio mewn ystyr cyffredinol.
         profile_directory: Mae'r cyfeiriadur proffil yn rhestru'r holl ddefnyddwyr sydd wedi dewis i fod yn ddarganfyddiadwy.
         require_invite_text: Pan fydd angen cymeradwyaeth â llaw ar gyfer cofrestriadau, gwnewch y “Pam ydych chi am ymuno?” mewnbwn testun yn orfodol yn hytrach na dewisol
@@ -131,17 +134,21 @@ cy:
         name: Dim ond er mwyn ei gwneud yn fwy darllenadwy y gallwch chi newid y llythrennau, er enghraifft
       terms_of_service:
         changelog: Mae modd ei strwythuro gyda chystrawen Markdown.
+        effective_date: Gall amserlen resymol amrywio rhwng 10 a 30 diwrnod o'r dyddiad y byddwch yn hysbysu'ch defnyddwyr.
         text: Mae modd ei strwythuro gyda chystrawen Markdown.
       terms_of_service_generator:
         admin_email: Mae hysbysiadau cyfreithiol yn cynnwys gwrth-hysbysiadau, gorchmynion llys, ceisiadau tynnu i lawr, a cheisiadau gorfodi'r gyfraith.
-        arbitration_address: Gall fod yr un peth â'r cyfeiriad ffisegol uchod, neu “D/A” os ydych chi'n defnyddio e-bost
-        arbitration_website: Gall fod yn ffurflen we, neu “D/A” os ydych chi'n defnyddio e-bost
+        arbitration_address: Gall fod yr un peth â'r cyfeiriad corfforol uchod, neu “D/A” os ydych chi'n defnyddio e-bost.
+        arbitration_website: Gall fod yn ffurflen we, neu “D/A” os ydych chi'n defnyddio e-bost.
+        choice_of_law: Dinas, rhanbarth, tiriogaeth neu wladwriaeth y bydd ei deddfau mewnol sylweddol yn llywodraethu unrhyw hawliadau a phob hawliad.
         dmca_address: Ar gyfer gweithredwyr yr Unol Daleithiau, defnyddiwch y cyfeiriad sydd wedi'i gofrestru yn Designated Agent Directory y DMCA. Mae rhestriad blychau post ar gael ar gais uniongyrchol, defnyddiwch gais Designated Agent Post Office Box Waiver Request y DMCA i anfon e-bost at y Swyddfa Hawlfraint a disgrifiwch eich bod yn gymedrolwr cynnwys yn y cartref sy'n ofni dial neu ddialedd am eich gweithredoedd ac sydd angen defnyddio Blwch P.O. i dynnu eich cyfeiriad cartref o olwg y cyhoedd.
-        dmca_email: Gall fod yr un e-bost sy'n cael ei ddefnyddio ar gyfer “Cyfeiriad e-bost ar gyfer hysbysiadau cyfreithiol” uchod
+        dmca_email: Gall fod yr un e-bost ag sy'n cael ei ddefnyddir ar gyfer “Cyfeiriad e-bost ar gyfer hysbysiadau cyfreithiol” uchod.
         domain: Dynodiad unigryw o'r gwasanaeth ar-lein rydych chi'n ei ddarparu.
         jurisdiction: Rhestrwch y wlad lle mae pwy bynnag sy'n talu'r biliau yn byw. Os yw'n gwmni neu'n endid arall, rhestrwch y wlad lle mae wedi'i ymgorffori, a'r ddinas, rhanbarth, tiriogaeth neu wladwriaeth fel y bo'n briodol.
+        min_age: Ni ddylai fod yn is na'r isafswm oedran sy'n ofynnol gan gyfreithiau eich awdurdodaeth.
       user:
         chosen_languages: Wedi eu dewis, dim ond tŵtiau yn yr ieithoedd hyn bydd yn cael eu harddangos mewn ffrydiau cyhoeddus
+        date_of_birth: Mae'n rhaid i ni sicrhau eich bod o leiaf %{age} i ddefnyddio Mastodon. Fyddwn ni ddim yn storio hwn.
         role: Mae'r rôl yn rheoli pa ganiatâd sydd gan y defnyddiwr.
       user_role:
         color: Lliw i'w ddefnyddio ar gyfer y rôl drwy'r UI, fel RGB mewn fformat hecs
@@ -155,6 +162,7 @@ cy:
         url: I ble bydd digwyddiadau'n cael eu hanfon
     labels:
       account:
+        attribution_domains: Gwefannau sy'n cael caniatâd i'ch cydnabod chi
         discoverable: Proffil nodwedd a phostiadau mewn algorithmau darganfod
         fields:
           name: Label
@@ -231,6 +239,7 @@ cy:
         setting_display_media_show_all: Dangos popeth
         setting_expand_spoilers: Dangos postiadau wedi'u marcio â rhybudd cynnwys bob tro
         setting_hide_network: Cuddio eich graff cymdeithasol
+        setting_missing_alt_text_modal: Dangos deialog cadarnhau cyn postio cyfrwng heb destun amgen
         setting_reduce_motion: Lleihau mudiant mewn animeiddiadau
         setting_system_font_ui: Defnyddio ffont rhagosodedig y system
         setting_system_scrollbars_ui: Defnyddiwch far sgrolio rhagosodedig y system
@@ -252,6 +261,7 @@ cy:
         name: Hashnod
       filters:
         actions:
+          blur: Cuddio cyfryngau gyda rhybudd
           hide: Cuddio'n llwyr
           warn: Cuddio â rhybudd
       form_admin_settings:
@@ -265,6 +275,7 @@ cy:
         favicon: Favicon
         mascot: Mascot cyfaddas (hen)
         media_cache_retention_period: Cyfnod cadw storfa cyfryngau
+        min_age: Gofyniad oed ieuengaf
         peers_api_enabled: Cyhoeddi rhestr o weinyddion a ddarganfuwyd yn yr API
         profile_directory: Galluogi cyfeiriadur proffil
         registrations_mode: Pwy all gofrestru
@@ -330,16 +341,22 @@ cy:
         usable: Caniatáu i bostiadau ddefnyddio'r hashnod hwn yn lleol
       terms_of_service:
         changelog: Beth sydd wedi newid?
+        effective_date: Dyddiad dod i rym
         text: Telerau Gwasanaeth
       terms_of_service_generator:
         admin_email: Cyfeiriad e-bost ar gyfer hysbysiadau cyfreithiol
         arbitration_address: Cyfeiriad ffisegol ar gyfer hysbysiadau cyflafareddu
         arbitration_website: Gwefan ar gyfer cyflwyno hysbysiadau cyflafareddu
+        choice_of_law: Dewis Cyfraith
         dmca_address: Cyfeiriad ffisegol ar gyfer DMCA/hysbysiadau hawlfraint
         dmca_email: Cyfeiriad e-bost ar gyfer DMCA/hysbysiadau hawlfraint
         domain: Parth
         jurisdiction: Awdurdodaeth gyfreithiol
+        min_age: Isafswm oedran
       user:
+        date_of_birth_1i: Dydd
+        date_of_birth_2i: Mis
+        date_of_birth_3i: Blwyddyn
         role: Rôl
         time_zone: Cylchfa amser
       user_role:
diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml
index 96bd1b3e70..91582e60c5 100644
--- a/config/locales/simple_form.da.yml
+++ b/config/locales/simple_form.da.yml
@@ -8,7 +8,7 @@ da:
         display_name: Dit fulde navn eller dit sjove navn.
         fields: Din hjemmeside, dine pronominer, din alder, eller hvad du har lyst til.
         indexable: Dine offentlige indlæg vil kunne vises i Mastodon-søgeresultater. Folk, som har interageret med dem, vil kunne finde dem uanset.
-        note: 'Du kan @omtale andre personer eller #hashtags.'
+        note: 'Du kan @omtale andre personer eller #etiketter.'
         show_collections: Folk vil ikke kunne tjekke dine Følger og Følgere. Folk, du selv følger, vil stadig kunne se dette.
         unlocked: Man vil kunne følges af folk uden først at godkende dem. Ønsker man at gennemgå Følg-anmodninger og individuelt acceptere/afvise nye følgere, så fjern markeringen.
       account_alias:
@@ -16,7 +16,7 @@ da:
       account_migration:
         acct: Angiv brugernavn@domain for den konto, hvortil du vil flytte
       account_warning_preset:
-        text: Du kan bruge indlægssyntaks, såsom URL'er, hashtags og omtaler
+        text: Du kan bruge indlægssyntaks, såsom URL'er, etiketter og omtaler
         title: Valgfri. Ikke synlig for modtageren
       admin_account_action:
         include_statuses: Brugeren vil se, hvilke indlæg, som har forårsaget modereringen/advarslen
@@ -54,7 +54,7 @@ da:
         password: Brug mindst 8 tegn
         phrase: Matches uanset uanset brug af store/små bogstaver i teksten eller indholdsadvarsel for et indlæg
         scopes: De API'er, som applikationen vil kunne tilgå. Vælges en topniveaudstrækning, vil detailvalg være unødvendige.
-        setting_aggregate_reblogs: Vis ikke nye boosts for nyligt boostede indlæg (påvirker kun nyligt modtagne boosts)
+        setting_aggregate_reblogs: Vis ikke nye fremhævelser for nyligt fremhævede indlæg (påvirker kun nyligt modtagne fremhævelser)
         setting_always_send_emails: Normalt sendes ingen e-mailnotifikationer under aktivt brug af Mastodon
         setting_default_sensitive: Sensitive medier er som standard skjult og kan vises med et klik
         setting_display_media_default: Skjul medier med sensitiv-markering
@@ -71,10 +71,11 @@ da:
         domain: Dette kan være domænenavnet vist i den benyttede i e-mailadresse eller MX-post. Begge tjekkes under tilmelding.
         with_dns_records: Et forsøg på at opløse det givne domænes DNS-poster foretages, og resultaterne blokeres ligeledes
       featured_tag:
-        name: 'Her er nogle af dine hyppigst brugte hashtags:'
+        name: 'Her er nogle af dine hyppigst brugte etiketter:'
       filters:
         action: Vælg handlingen til eksekvering, når et indlæg matcher filteret
         actions:
+          blur: Skjul medier bag en advarsel, uden at skjule selve teksten
           hide: Skjul filtreret indhold helt (adfærd som om, det ikke fandtes)
           warn: Skjul filtreret indhold bag en advarsel, der nævner filterets titel
       form_admin_settings:
@@ -83,11 +84,12 @@ da:
         backups_retention_period: Brugere har mulighed for at generere arkiver af deres indlæg til senere downloade. Når sat til positiv værdi, vil disse arkiver automatisk blive slettet fra lagerpladsen efter det angivne antal dage.
         bootstrap_timeline_accounts: Disse konti fastgøres øverst på nye brugeres følg-anbefalinger.
         closed_registrations_message: Vises, når tilmeldinger er lukket
-        content_cache_retention_period: Alle indlæg fra andre servere (herunder boosts og besvarelser) slettes efter det angivne antal dage uden hensyn til lokal brugerinteraktion med disse indlæg. Dette omfatter indlæg, hvor en lokal bruger har markeret dem som bogmærker eller favoritter. Private omtaler mellem brugere fra forskellige instanser vil også være tabt og umulige at gendanne. Brugen af denne indstilling er beregnet til særlige formål instanser og bryder mange brugerforventninger ved implementering til almindelig brug.
+        content_cache_retention_period: Alle indlæg fra andre servere (herunder fremhævelser og besvarelser) slettes efter det angivne antal dage uden hensyn til lokal brugerinteraktion med disse indlæg. Dette omfatter indlæg, hvor en lokal bruger har markeret dem som bogmærker eller favoritter. Private omtaler mellem brugere fra forskellige instanser vil også være tabt og umulige at gendanne. Brugen af denne indstilling er beregnet til særlige formål instanser og bryder mange brugerforventninger ved implementering til almindelig brug.
         custom_css: Man kan anvende tilpassede stilarter på Mastodon-webversionen.
         favicon: WEBP, PNG, GIF eller JPG. Tilsidesætter standard Mastodon favikonet på mobilenheder med et tilpasset ikon.
         mascot: Tilsidesætter illustrationen i den avancerede webgrænseflade.
         media_cache_retention_period: Mediefiler fra indlæg oprettet af eksterne brugere er cachet på din server. Når sat til positiv værdi, slettes medier efter det angivne antal dage. Anmodes om mediedata efter de er slettet, gendownloades de, hvis kildeindholdet stadig er tilgængeligt. Grundet begrænsninger på, hvor ofte linkforhåndsvisningskort forespørger tredjeparts websteder, anbefales det at sætte denne værdi til mindst 14 dage, ellers opdateres linkforhåndsvisningskort ikke efter behov før det tidspunkt.
+        min_age: Brugere anmodes om at bekræfte deres fødselsdato under tilmelding
         peers_api_enabled: En liste med domænenavne, som denne server har stødt på i fediverset. Ingen data inkluderes her om, hvorvidt der fødereres med en given server, blot at din server kender til det. Dette bruges af tjenester, som indsamler generelle føderationsstatistikker.
         profile_directory: Profilmappen oplister alle brugere, som har valgt at kunne opdages.
         require_invite_text: Når tilmelding kræver manuel godkendelse, så gør “Hvorfor ønsker du at deltage?” tekstinput obligatorisk i stedet for valgfrit
@@ -102,7 +104,7 @@ da:
         thumbnail: Et ca. 2:1 billede vist sammen med serveroplysningerne.
         timeline_preview: Udloggede besøgende kan gennemse serverens seneste offentlige indlæg.
         trendable_by_default: Spring manuel gennemgang af trendindhold over. Individuelle elementer kan stadig fjernes fra trends efter kendsgerningen.
-        trends: Tendenser viser, hvilke indlæg, hashtags og nyheder opnår momentum på serveren.
+        trends: Tendenser viser, hvilke indlæg, etiketter og nyheder opnår momentum på serveren.
         trends_as_landing_page: Vis tendensindhold til udloggede brugere og besøgende i stedet for en beskrivelse af denne server. Kræver, at tendenser er aktiveret.
       form_challenge:
         current_password: Du bevæger dig ind på et sikkert område
@@ -132,17 +134,21 @@ da:
         name: Kun bogstavtyper (store/små) kan ændres, eksempelvis for at gøre det mere læsbart
       terms_of_service:
         changelog: Kan struktureres med Markdown-syntaks.
+        effective_date: En rimelig tidsramme kan variere fra 10 til 30 dage fra den dato, hvor man underretter sine brugere.
         text: Kan struktureres med Markdown-syntaks.
       terms_of_service_generator:
         admin_email: Juridiske bekendtgørelser omfatter imødegåelsesbekendtgørelser, retskendelser, nedtagelses- og retshåndhævelsesanmodninger.
-        arbitration_address: Kan være den samme som Fysisk adresse ovenfor, eller “N/A” ved brug af e-mail
-        arbitration_website: Kan være en webformular, eller “N/A” ved brug af e-mail
+        arbitration_address: Kan være den samme som Fysisk adresse ovenfor, eller “Ingen” ved brug af e-mail.
+        arbitration_website: Kan være en webformular, eller “Intet” ved brug af e-mail.
+        choice_of_law: By, region, område eller angiv de interne materielle love, som vil være gældende for samtlige krav.
         dmca_address: For amerikanske operatører, brug den adresse, der er registreret i DMCA Designated Agent Directory. En Postboksliste er tilgængelig på direkte anmodning, brug den DMCA Designated Agent Post Office Box Waiver Request til at e-maile Ophavsretskontoret og beskrive, at man er en hjemmebaseret indholdsmoderator, der frygter hævn eller gengældelse for sine handlinger og behøver en Postboks for at fjerne hjemmeadressen fra den offentlige visning.
-        dmca_email: Kan være samme e-mail, som brugt til “E-mailadresse til juridiske meddelelser” ovenfor
+        dmca_email: Kan være samme e-mail, som brugt til “E-mailadresse til juridiske meddelelser” ovenfor.
         domain: Unik identifikation af den onlinetjeneste, man leverer.
         jurisdiction: Angiv landet, hvor betaleren af regningerne er bosiddende. Er det en virksomhed eller en anden entitet, angiv det land, hvor det er stiftet, og byen, regionen, området eller staten efter behov.
+        min_age: Bør ikke være under den iht. lovgivningen i det aktuelle retsområde krævede minimumsalder.
       user:
         chosen_languages: Når markeret, vil kun indlæg på de valgte sprog fremgå på offentlige tidslinjer
+        date_of_birth: Vi er nødt til at sikre, at man er fyldt %{age} for at bruge Mastodon. Vi gemmer ikke denne information.
         role: Rollen styrer, hvilke tilladelser brugeren er tildelt.
       user_role:
         color: Farven, i RGB hex-format, der skal bruges til rollen i hele UI'en
@@ -217,10 +223,10 @@ da:
         password: Adgangskode
         phrase: Nøgleord/-sætning
         setting_advanced_layout: Aktivér avanceret webgrænseflade
-        setting_aggregate_reblogs: Gruppér boosts på tidslinjer
+        setting_aggregate_reblogs: Gruppér fremhævelser på tidslinjer
         setting_always_send_emails: Send altid e-mailnotifikationer
         setting_auto_play_gif: Autoafspil animerede GIF'er
-        setting_boost_modal: Vis bekræftelsesdialog inden boosting
+        setting_boost_modal: Vis bekræftelsesdialog inden fremhævelse
         setting_default_language: Sprog for indlæg
         setting_default_privacy: Fortrolighed for indlæg
         setting_default_sensitive: Markér altid medier som sensitive
@@ -255,6 +261,7 @@ da:
         name: Hashtag
       filters:
         actions:
+          blur: Skjul medier med en advarsel
           hide: Skjul helt
           warn: Skjul bag en advarsel
       form_admin_settings:
@@ -268,6 +275,7 @@ da:
         favicon: Favikon
         mascot: Tilpasset maskot (ældre funktion)
         media_cache_retention_period: Media-cache opbevaringsperiode
+        min_age: Minimums alderskrav
         peers_api_enabled: Udgiv liste over fundne server i API'en
         profile_directory: Aktivér profiloversigt
         registrations_mode: Hvem, der kan tilmelde sig
@@ -311,7 +319,7 @@ da:
         follow_request: Nogen anmodede om at følge dig
         mention: Nogen omtalte dig
         pending_account: Ny konto kræver gennemgang
-        reblog: Nogen boostede dit indlæg
+        reblog: Nogen fremhævede dit indlæg
         report: Ny anmeldelse indsendt
         software_updates:
           all: Notificér ved alle opdateringer
@@ -327,22 +335,28 @@ da:
         indexable: Inkludér profilside i søgemaskiner
         show_application: Vis, fra hvilken app et indlæg er sendt
       tag:
-        listable: Tillad visning af dette hashtag i søgninger og forslag
+        listable: Tillad visning af denne etiket i søgninger og forslag
         name: Hashtag
-        trendable: Tillad visning af dette hashtag under trends
-        usable: Tillad indlæg at benytte dette hashtag lokalt
+        trendable: Tillad visning af denne etiket under tendenser
+        usable: Tillad indlæg at benytte denne etiket lokalt
       terms_of_service:
         changelog: Hvad der er ændret?
+        effective_date: Ikrafttrædelsesdato
         text: Tjenestevilkår
       terms_of_service_generator:
         admin_email: E-mailadresse til juridiske meddelelser
         arbitration_address: Fysisk adresse til voldgiftsmeddelelser
         arbitration_website: Websted til indsendelse af voldgiftsmeddelelser
+        choice_of_law: Lovvalg
         dmca_address: Fysisk adresse til DMCA-/ophavsretsmeddelelser
         dmca_email: E-mailadresse til DMCA-/ophavsretsmeddelelser
         domain: Domæne
         jurisdiction: Juridisk jurisdiktion
+        min_age: Minimumsalder
       user:
+        date_of_birth_1i: Dag
+        date_of_birth_2i: Måned
+        date_of_birth_3i: År
         role: Rolle
         time_zone: Tidszone
       user_role:
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index 9f3f84ffce..342a1dbe1c 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -75,6 +75,7 @@ de:
       filters:
         action: Gib an, welche Aktion ausgeführt werden soll, wenn ein Beitrag dem Filter entspricht
         actions:
+          blur: Medien mit einer Warnung ausblenden, ohne den Text selbst auszublenden
           hide: Den gefilterten Beitrag vollständig ausblenden, als hätte er nie existiert
           warn: Den gefilterten Beitrag hinter einer Warnung, die den Filtertitel beinhaltet, ausblenden
       form_admin_settings:
@@ -83,11 +84,12 @@ de:
         backups_retention_period: Nutzer*innen haben die Möglichkeit, Archive ihrer Beiträge zu erstellen, die sie später herunterladen können. Wenn ein positiver Wert gesetzt ist, werden diese Archive nach der festgelegten Anzahl von Tagen automatisch aus deinem Speicher gelöscht.
         bootstrap_timeline_accounts: Diese Konten werden bei den Follower-Empfehlungen für neu registrierte Nutzer*innen oben angeheftet.
         closed_registrations_message: Wird angezeigt, wenn Registrierungen deaktiviert sind
-        content_cache_retention_period: Sämtliche Beiträge von anderen Servern (einschließlich geteilte Beiträge und Antworten) werden, unabhängig von der Interaktion der lokalen Nutzer*innen mit diesen Beiträgen, nach der festgelegten Anzahl von Tagen gelöscht. Das betrifft auch Beiträge, die von lokalen Nutzer*innen favorisiert oder als Lesezeichen gespeichert wurden. Private Erwähnungen zwischen Nutzer*innen von verschiedenen Servern werden ebenfalls verloren gehen und können nicht wiederhergestellt werden. Das Verwenden dieser Option richtet sich ausschließlich an Server für spezielle Zwecke und wird die allgemeine Nutzungserfahrung beeinträchtigen, wenn sie für den allgemeinen Gebrauch aktiviert ist.
+        content_cache_retention_period: Sämtliche Beiträge von anderen Servern (einschließlich geteilte Beiträge und Antworten) werden, unabhängig von der Interaktion der lokalen Nutzer*innen mit diesen Beiträgen, nach der festgelegten Anzahl von Tagen gelöscht. Das betrifft auch Beiträge, die von lokalen Nutzer*innen favorisiert oder als Lesezeichen gespeichert wurden. Private Erwähnungen zwischen Nutzer*innen von verschiedenen Servern werden ebenfalls verloren gehen und können nicht wiederhergestellt werden. Diese Option richtet sich ausschließlich an Server mit speziellen Zwecken und wird die allgemeine Nutzungserfahrung beeinträchtigen, wenn sie für den allgemeinen Gebrauch aktiviert ist.
         custom_css: Du kannst benutzerdefinierte Stile auf die Web-Version von Mastodon anwenden.
         favicon: WEBP, PNG, GIF oder JPG. Überschreibt das Standard-Mastodon-Favicon mit einem eigenen Symbol.
         mascot: Überschreibt die Abbildung in der erweiterten Weboberfläche.
         media_cache_retention_period: Mediendateien aus Beiträgen von externen Nutzer*innen werden auf deinem Server zwischengespeichert. Wenn ein positiver Wert gesetzt ist, werden die Medien nach der festgelegten Anzahl von Tagen gelöscht. Sollten die Medien nach dem Löschvorgang wieder angefragt werden, werden sie erneut heruntergeladen, sofern der ursprüngliche Inhalt noch vorhanden ist. Es wird empfohlen, diesen Wert auf mindestens 14 Tage festzulegen, da die Häufigkeit der Abfrage von Linkvorschaukarten für Websites von Dritten begrenzt ist und die Linkvorschaukarten sonst nicht vor Ablauf dieser Zeit aktualisiert werden.
+        min_age: Nutzer*innen werden bei der Registrierung aufgefordert, ihr Geburtsdatum zu bestätigen
         peers_api_enabled: Eine Liste von Domains, die diesem Server im Fediverse begegnet sind. Hierbei werden keine Angaben darüber gemacht, ob du mit einem bestimmten Server föderierst, sondern nur, dass dein Server davon weiß. Dies wird von Diensten verwendet, die allgemein Statistiken übers Ferdiverse sammeln.
         profile_directory: Dieses Verzeichnis zeigt alle Profile an, die sich dafür entschieden haben, entdeckt zu werden.
         require_invite_text: Wenn Registrierungen eine manuelle Genehmigung erfordern, dann werden Nutzer einen Grund für ihre Registrierung angeben müssen
@@ -132,17 +134,21 @@ de:
         name: Du kannst nur die Groß- und Kleinschreibung der Buchstaben ändern, um es z. B. lesbarer zu machen
       terms_of_service:
         changelog: Kann mit der Markdown-Syntax formatiert werden.
+        effective_date: Ein angemessener Zeitraum liegt zwischen 10 und 30 Tagen, nachdem deine Nutzer*innen benachrichtigt wurden.
         text: Kann mit der Markdown-Syntax formatiert werden.
       terms_of_service_generator:
         admin_email: Rechtliche Hinweise umfassen Gegendarstellungen, Gerichtsbeschlüsse, Anfragen zum Herunternehmen von Inhalten und Anfragen von Strafverfolgungsbehörden.
-        arbitration_address: Kann wie die Anschrift hierüber sein oder „N/A“, falls eine E-Mail verwendet wird
-        arbitration_website: Kann ein Webformular sein oder „N/A“, falls eine E-Mail verwendet wird
+        arbitration_address: Kann wie die Anschrift hierüber oder „nicht verfügbar“ sein, sollte eine E-Mail-Adresse verwendet werden.
+        arbitration_website: Kann ein Web-Formular oder „nicht verfügbar“ sein, sollte eine E-Mail-Adresse verwendet werden.
+        choice_of_law: Stadt, Region, Gebiet oder Staat, dessen innerstaatliches materielles Recht für alle Ansprüche maßgebend ist.
         dmca_address: US-Betreiber sollten die im „DMCA Designated Agent Directory“ eingetragene Adresse verwenden. Eine Postfachadresse ist auf direkte Anfrage verfügbar. Verwenden Sie die „DMCA Designated Agent Post Box Waiver“-Anfrage, um per E-Mail die Urheberrechtsbehörde darüber zu unterrichten, dass Sie Inhalte per Heimarbeit moderieren, eventuelle Rache oder Vergeltung für Ihre Handlungen befürchten und deshalb eine Postfachadresse benötigen, um Ihre Privatadresse nicht preiszugeben.
-        dmca_email: Kann dieselbe E-Mail wie bei „E-Mail-Adresse für rechtliche Hinweise“ sein
+        dmca_email: Kann dieselbe E-Mail wie bei „E-Mail-Adresse für rechtliche Hinweise“ sein.
         domain: Einzigartige Identifizierung des angebotenen Online-Services.
         jurisdiction: Gib das Land an, in dem die Person lebt, die alle Rechnungen bezahlt. Falls es sich dabei um ein Unternehmen oder eine andere Einrichtung handelt, gib das Land mit dem Sitz an, sowie die Stadt oder Region.
+        min_age: Sollte nicht unter dem gesetzlich vorgeschriebenen Mindestalter liegen.
       user:
         chosen_languages: Wenn du hier eine oder mehrere Sprachen auswählst, werden ausschließlich Beiträge in diesen Sprachen in deinen öffentlichen Timelines angezeigt
+        date_of_birth: Wir müssen sicherstellen, dass du mindestens %{age} Jahre alt bist, um Mastodon verwenden zu können. Das Alter wird nicht gespeichert.
         role: Die Rolle bestimmt, welche Berechtigungen das Konto hat.
       user_role:
         color: Farbe, die für diese Rolle in der gesamten Benutzerschnittstelle verwendet wird, als RGB im Hexadezimalsystem
@@ -255,8 +261,9 @@ de:
         name: Hashtag
       filters:
         actions:
+          blur: Medien mit einer Warnung ausblenden
           hide: Vollständig ausblenden
-          warn: Mit einer Inhaltswarnung ausblenden
+          warn: Mit einer Warnung ausblenden
       form_admin_settings:
         activity_api_enabled: Aggregierte Nutzungsdaten über die API veröffentlichen
         app_icon: App-Symbol
@@ -268,6 +275,7 @@ de:
         favicon: Favicon
         mascot: Benutzerdefiniertes Maskottchen (Legacy)
         media_cache_retention_period: Aufbewahrungsfrist für Medien im Cache
+        min_age: Erforderliches Mindestalter
         peers_api_enabled: Die entdeckten Server im Fediverse über die API veröffentlichen
         profile_directory: Profilverzeichnis aktivieren
         registrations_mode: Wer darf ein neues Konto registrieren?
@@ -333,16 +341,22 @@ de:
         usable: Beiträge dürfen diesen Hashtag lokal verwenden
       terms_of_service:
         changelog: Was hat sich geändert?
+        effective_date: Datum des Inkrafttretens
         text: Nutzungsbedingungen
       terms_of_service_generator:
         admin_email: E-Mail-Adresse für rechtliche Hinweise
         arbitration_address: Anschrift für Schiedsverfahren
         arbitration_website: Website zum Einreichen von Schiedsverfahren
+        choice_of_law: Wahl der Rechtsordnung
         dmca_address: Anschrift für Urheberrechtsverletzungen
         dmca_email: E-Mail-Adresse für Urheberrechtsverletzungen
         domain: Domain
         jurisdiction: Gerichtsstand
+        min_age: Mindestalter
       user:
+        date_of_birth_1i: Tag
+        date_of_birth_2i: Monat
+        date_of_birth_3i: Jahr
         role: Rolle
         time_zone: Zeitzone
       user_role:
diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml
index 4b6cc80a12..62de683213 100644
--- a/config/locales/simple_form.el.yml
+++ b/config/locales/simple_form.el.yml
@@ -135,10 +135,7 @@ el:
         text: Μπορεί να δομηθεί με σύνταξη Markdown.
       terms_of_service_generator:
         admin_email: Οι νομικές ανακοινώσεις περιλαμβάνουν αντικρούσεις, δικαστικές αποφάσεις, αιτήματα που έχουν ληφθεί και αιτήματα επιβολής του νόμου.
-        arbitration_address: Μπορεί να είναι το ίδιο με τη φυσική διεύθυνση παραπάνω, ή “N/A” εάν χρησιμοποιείται email
-        arbitration_website: Μπορεί να είναι μια φόρμα ιστού, ή “N/A” εάν χρησιμοποιείται email
         dmca_address: Για τους φορείς των ΗΠΑ, χρησιμοποιήστε τη διεύθυνση που έχει καταχωρηθεί στο DMCA Designated Agent Directory. A P.O. Η λίστα είναι διαθέσιμη κατόπιν απευθείας αιτήματος, Χρησιμοποιήστε το αίτημα απαλλαγής από την άδεια χρήσης του καθορισμένου από το DMCA Agent Post Office Box για να στείλετε email στο Γραφείο Πνευματικών Δικαιωμάτων και περιγράψτε ότι είστε συντονιστής περιεχομένου με βάση το σπίτι, ο οποίος φοβάται την εκδίκηση ή την απόδοση για τις ενέργειές σας και πρέπει να χρησιμοποιήσετε ένα P.. Box για να αφαιρέσετε τη διεύθυνση οικίας σας από τη δημόσια προβολή.
-        dmca_email: Μπορεί να είναι το ίδιο email που χρησιμοποιείται για “Διεύθυνση email για νομικές ανακοινώσεις” παραπάνω
         domain: Μοναδικό αναγνωριστικό της διαδικτυακής υπηρεσίας που παρέχεις.
         jurisdiction: Ανέφερε τη χώρα όπου ζει αυτός που πληρώνει τους λογαριασμούς. Εάν πρόκειται για εταιρεία ή άλλη οντότητα, ανέφερε τη χώρα όπου υφίσταται, και την πόλη, περιοχή, έδαφος ή πολιτεία ανάλογα με την περίπτωση.
       user:
@@ -233,6 +230,7 @@ el:
         setting_display_media_show_all: Εμφάνιση όλων
         setting_expand_spoilers: Μόνιμη ανάπτυξη των τουτ με προειδοποίηση περιεχομένου
         setting_hide_network: Κρύψε τις διασυνδέσεις σου
+        setting_missing_alt_text_modal: Εμφάνιση διαλόγου επιβεβαίωσης πριν από την δημοσίευση πολυμέσων χωρίς alt κείμενο
         setting_reduce_motion: Μείωση κίνησης κινουμένων στοιχείων
         setting_system_font_ui: Χρήση της προεπιλεγμένης γραμματοσειράς του συστήματος
         setting_system_scrollbars_ui: Χρήση προκαθορισμένης γραμμής κύλισης του συστήματος
diff --git a/config/locales/simple_form.en-GB.yml b/config/locales/simple_form.en-GB.yml
index fa7868cc76..5f5453edc3 100644
--- a/config/locales/simple_form.en-GB.yml
+++ b/config/locales/simple_form.en-GB.yml
@@ -135,10 +135,7 @@ en-GB:
         text: Can be structured with Markdown syntax.
       terms_of_service_generator:
         admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests.
-        arbitration_address: Can be the same as Physical address above, or “N/A” if using email
-        arbitration_website: Can be a web form, or “N/A” if using email
         dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
-        dmca_email: Can be the same email used for “Email address for legal notices” above
         domain: Unique identification of the online service you are providing.
         jurisdiction: List the country where whoever pays the bills lives. If it’s a company or other entity, list the country where it’s incorporated, and the city, region, territory or state as appropriate.
       user:
@@ -233,6 +230,7 @@ en-GB:
         setting_display_media_show_all: Show all
         setting_expand_spoilers: Always expand posts marked with content warnings
         setting_hide_network: Hide your social graph
+        setting_missing_alt_text_modal: Show confirmation dialogue before posting media without alt text
         setting_reduce_motion: Reduce motion in animations
         setting_system_font_ui: Use system's default font
         setting_system_scrollbars_ui: Use system's default scrollbar
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 4cb3ac260c..ff17536ee1 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -66,18 +66,18 @@ en:
         setting_custom_css_lead: 'Be sure to remember: In the unlikely event that you make a mistake in entering your custom CSS and the screen does not display properly, you can disable your custom CSS from the link at the bottom of the sign-in screen. Open the sign-in screen in private mode of your browser, for example, and disable it.'
         setting_default_searchability: On kmyblue and Fedibird, the search is based on the search permission setting; on Misskey, all public, local public, and non-public posts are searched regardless of this setting; on Mastodon and Firefish, instead of search permission, the "Make public posts freely searchable on other servers" setting in the profile settings is applied. In Mastodon and Firefish, the "Make public posts freely searchable on other servers" setting in the profile settings is applied instead of the search permission.
         setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click
-        setting_disallow_unlisted_public_searchability: この設定を有効にすると、非収載投稿と検索範囲「誰でも」は両立できず不特定多数からの検索が不可になります。Fedibirdと同じ挙動になります
+        setting_disallow_unlisted_public_searchability: If you enable this setting, unlisted posts and the “everyone” search scope cannot coexist, making it impossible for unspecified users to search your posts.
         setting_display_media_default: Hide media marked as sensitive
         setting_display_media_hide_all: Always hide media
         setting_display_media_show_all: Always show media
         setting_dtl_force_searchability: 'With using #%{tag} tag, your post settings will be changed forcibly'
         setting_dtl_force_visibility: 'With using #%{tag} tag, your post settings will be changed forcibly'
         setting_emoji_reaction_policy: Even with this setting, users on non-kmyblue servers are free to put their emoji reaction on the post and share it within the same server. If you simply want to remove the emoji reaction from your own screen, you can disable it from the appearance settings
-        setting_emoji_reaction_streaming_notify_impl2: 当該サーバーの独自機能に対応したアプリを利用時に、絵文字リアクション機能を利用できます。動作確認していないため(そもそもそのようなアプリ自体を確認できていないため)正しく動かない場合があります
+        setting_emoji_reaction_streaming_notify_impl2: You can use the emoji reaction feature when using an app that supports this server’s unique functionality. However, since this has not been tested (and such apps have not even been identified), it may not work correctly.
         setting_enable_emoji_reaction: If turn off, other users still can react your posts
         setting_enabled_visibilities: If turn off, you cannot select and post the privacy.
-        setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
-        setting_public_post_to_unlisted: 未対応のサードパーティアプリからもローカル公開で投稿できますが、公開投稿はWeb以外できなくなります
+        setting_hide_network: It will hide the following and follower information from the profile page.
+        setting_public_post_to_unlisted: You can still post with local visibility using unsupported third-party apps, but public posts will no longer be possible outside of the web interface.
         setting_reject_send_limited_to_suspects: This applies to "Mutual Only" posts. Circle posts will be delivered without exception. Some Misskey servers have independently supported limited posting, but this is a setting for those who are concerned about it, as mutual-only posting exposes some of the users you are mutual with to Misskey users!
         setting_reject_unlisted_subscription: Misskey and its forks can **subscribe and search** for "non-following" posts from accounts they do not follow. This differs from kmyblue's behavior. It delivers posts in the specified public range to such servers as "followers only". Please understand, however, that due to its structure, it is difficult to handle perfectly and will occasionally be delivered as non-subscribed.
         setting_reverse_search_quote: Double-quotes will result in a search with a wider range of notation, which is the opposite of Mastodon's default behavior.
@@ -99,6 +99,7 @@ en:
       filters:
         action: Chose which action to perform when a post matches the filter
         actions:
+          blur: Hide media behind a warning, without hiding the text itself
           hide: Completely hide the filtered content, behaving as if it did not exist
           warn: Hide the filtered content behind a warning mentioning the filter's title
       form_admin_settings:
@@ -116,13 +117,14 @@ en:
         favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon.
         mascot: Overrides the illustration in the advanced web interface.
         media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time.
+        min_age: Users will be asked to confirm their date of birth during sign-up
         peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
         profile_directory: The profile directory lists all users who have opted-in to be discoverable.
         receive_other_servers_emoji_reaction: It can cause load. It is recommended to enable it only when there are few people.
-        registrations_end_hour: 新規登録が承認なしで可能な時間帯の開始時間を指定します。これより前の時間に登録することはできません。終了時間より後にすることはできません。この時間帯から外れた新規登録には、別途承認が必要となります。
-        registrations_limit: 現在のユーザー数がこれを超過すると、管理者がこの数値を増やさない限り新規登録できません。0を指定すると、この制限を無効化します。
-        registrations_limit_per_day: 本日登録されたユーザー数がこれを超過すると、UTC時刻で翌日0時にならない限り新規登録できません。0を指定すると、この制限を無効化します。
-        registrations_start_hour: 新規登録が承認なしで可能な時間帯の終了時間を指定します。これより後の時間に登録することはできません。開始時間より前にすることはできません。この時間帯から外れた新規登録には、別途承認が必要となります。
+        registrations_end_hour: Specifies the end of the time window during which new registrations can be made without approval. Registrations cannot be made before the start time or after this end time. Registrations outside this time window will require separate approval.
+        registrations_limit: If the current number of users exceeds this value, new registrations will not be possible unless the administrator increases the limit. Setting this to 0 disables the restriction.        
+        registrations_limit_per_day: If the number of users registered today exceeds this value, no new registrations will be allowed until 00:00 UTC the next day. Setting this to 0 disables the restriction.
+        registrations_start_hour: Specifies the start of the time window during which new registrations can be made without approval. Registrations cannot be made before this time or after the end time. Registrations outside this time window will require separate approval.
         require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
         site_contact_email: How people can reach you for legal or support inquiries.
         site_contact_username: How people can reach you on Mastodon.
@@ -166,17 +168,21 @@ en:
         name: You can only change the casing of the letters, for example, to make it more readable
       terms_of_service:
         changelog: Can be structured with Markdown syntax.
+        effective_date: A reasonable timeframe can range anywhere from 10 to 30 days from the date you notify your users.
         text: Can be structured with Markdown syntax.
       terms_of_service_generator:
         admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests.
-        arbitration_address: Can be the same as Physical address above, or “N/A” if using email
-        arbitration_website: Can be a web form, or “N/A” if using email
+        arbitration_address: Can be the same as Physical address above, or “N/A” if using email.
+        arbitration_website: Can be a web form, or “N/A” if using email.
+        choice_of_law: City, region, territory or state the internal substantive laws of which shall govern any and all claims.
         dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
-        dmca_email: Can be the same email used for “Email address for legal notices” above
+        dmca_email: Can be the same email used for “Email address for legal notices” above.
         domain: Unique identification of the online service you are providing.
         jurisdiction: List the country where whoever pays the bills lives. If it’s a company or other entity, list the country where it’s incorporated, and the city, region, territory or state as appropriate.
+        min_age: Should not be below the minimum age required by the laws of your jurisdiction.
       user:
         chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
+        date_of_birth: We have to make sure you're at least %{age} to use Mastodon. We won't store this.
         role: The role controls which permissions the user has.
       user_role:
         color: Color to be used for the role throughout the UI, as RGB in hex format
@@ -195,8 +201,8 @@ en:
         discoverable: Feature profile and posts in discovery algorithms
         fields:
           examples:
-            name_1: 例) GitHub
-            value_1: 例) https://github.com/xxxxxx
+            name_1: Example Gitea
+            value_1: Example https://giteahub.com
           name: Label
           value: Content
         indexable: Include public posts in search results
@@ -373,6 +379,7 @@ en:
         name: Hashtag
       filters:
         actions:
+          blur: Hide media with a warning
           hide: Hide completely
           warn: Hide with a warning
         options:
@@ -397,6 +404,7 @@ en:
         favicon: Favicon
         mascot: Custom mascot (legacy)
         media_cache_retention_period: Media cache retention period
+        min_age: Minimum age requirement
         peers_api_enabled: Publish list of discovered servers in the API
         profile_directory: Enable profile directory
         receive_other_servers_emoji_reaction: Receive emoji reaction between other server users
@@ -475,16 +483,22 @@ en:
         usable: Allow posts to use this hashtag locally
       terms_of_service:
         changelog: What's changed?
+        effective_date: Effective date
         text: Terms of Service
       terms_of_service_generator:
         admin_email: Email address for legal notices
         arbitration_address: Physical address for arbitration notices
         arbitration_website: Website for submitting arbitration notices
+        choice_of_law: Choice of Law
         dmca_address: Physical address for DMCA/copyright notices
         dmca_email: Email address for DMCA/copyright notices
         domain: Domain
         jurisdiction: Legal jurisdiction
+        min_age: Minimum age
       user:
+        date_of_birth_1i: Day
+        date_of_birth_2i: Month
+        date_of_birth_3i: Year
         role: Role
         time_zone: Time zone
       user_role:
diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml
index 7aafd666a5..1331ea7333 100644
--- a/config/locales/simple_form.eo.yml
+++ b/config/locales/simple_form.eo.yml
@@ -56,10 +56,10 @@ eo:
         scopes: Kiujn API-ojn la aplikaĵo permesiĝos atingi. Se vi elektas supran amplekson, vi ne bezonas elekti la individuajn.
         setting_aggregate_reblogs: Ne montri novajn plusendojn de mesaĝoj lastatempe plusenditaj (nur efikas al nove ricevitaj plusendoj)
         setting_always_send_emails: Normale, la sciigoj per retpoŝto ne estos senditaj kiam vi uzas Mastodon aktive
-        setting_default_sensitive: Tiklaj plurmedioj estas kaŝitaj implicite, kaj povas esti montritaj per klako
+        setting_default_sensitive: Tiklaj vidaŭdaĵoj estas implicite kaŝitaj kaj povas esti montritaj per alklako
         setting_display_media_default: Kaŝi plurmediojn markitajn kiel tiklaj
-        setting_display_media_hide_all: Ĉiam kaŝi la plurmediojn
-        setting_display_media_show_all: Ĉiam montri la plurmediojn
+        setting_display_media_hide_all: Ĉiam kaŝi la vidaŭdaĵojn
+        setting_display_media_show_all: Ĉiam montri la vidaŭdaĵojn
         setting_system_scrollbars_ui: Aplikas nur por surtablaj retumiloj baziĝas de Safari kaj Chrome
         setting_use_blurhash: Transirojn estas bazita sur la koloroj de la kaŝitaj aŭdovidaĵoj sed ne montri iun ajn detalon
         setting_use_pending_items: Kaŝi tempoliniajn ĝisdatigojn malantaŭ klako anstataŭ aŭtomate rulumi la fluon
@@ -75,6 +75,7 @@ eo:
       filters:
         action: Elekti ago kiam mesaĝo kongruas la filtrilon
         actions:
+          blur: Kaŝi amaskomunikilaron malantaŭ averto, sen kaŝi la tekston mem
           hide: Tute kaŝigi la filtritajn enhavojn, kvazau ĝi ne ekzistis
           warn: Kaŝi la enhavon filtritan malantaŭ averto mencianta la nomon de la filtro
       form_admin_settings:
@@ -132,17 +133,17 @@ eo:
         name: Vi povas ŝanĝi nur la majuskladon de la literoj, ekzemple, por igi ĝin pli legebla
       terms_of_service:
         changelog: Povas esti strukturita per sintakso Markdown-a.
+        effective_date: Racia tempodaŭro povas varii ie ajn de 10 ĝis 30 tagoj de la dato, kiam vi sciigas viajn uzantojn.
         text: Povas esti strukturita per sintakso Markdown-a.
       terms_of_service_generator:
         admin_email: Legalaj sciigoj povas esti kontraŭsciigoj, postulaĵoj de tribunalo, postulaĵoj pri forigo, kaj postulaĵoj de la policaro.
-        arbitration_address: Povas esti la sama kiel Fizika adreso supre, aŭ "N/A" se oni uzas retpoŝton
-        arbitration_website: Povas esti retformo, aŭ "N/A" se oni uzas retpoŝton
+        choice_of_law: Urbo, regiono, teritorio aŭ ŝtato, kies internaj substantivaj leĝoj regos iujn kaj ĉiujn asertojn.
         dmca_address: Por tenantoj en Usono, uzu la adreson registritan en la DMCA Designated Agenŭ Directory. Registrolibro de poŝtskatoloj haveblas per direkta postulo, uzu la DMCA Designated Agent Post Office Box Waiver Request por retpoŝti la Ofico de Kopirajto kaj priskribu, ke vi estas hejm-trovigita administranto por enhavo kaj devas uzi Poŝtskatolon por forigi vian hejmadreson de publika vido.
-        dmca_email: Povas esti la sama retpoŝtadreso uzita por "Retpoŝtadreso por legalaj sciigoj" supre
         domain: Unika identigilo de la retaj servicoj, ke vi provizas.
         jurisdiction: Enlistigu la landon, kie loĝas kiu pagas la fakturojn. Se ĝi estas kompanio aŭ alia ento, listigu la landon, kie ĝi estas enkorpigita, kaj la urbon, regionon, teritorion aŭ ŝtaton laŭeble.
       user:
         chosen_languages: Kun tio markita nur mesaĝoj en elektitaj lingvoj aperos en publikaj tempolinioj
+        date_of_birth: Ni devas certigi, ke vi estas almenaŭ %{age} por uzi Mastodon. Ni ne konservos ĉi tion.
         role: La rolo kontrolas kiujn permesojn la uzanto havas.
       user_role:
         color: Koloro uzita por la rolo sur la UI, kun RGB-formato
@@ -193,7 +194,7 @@ eo:
         text: Klarigu kial ĉi tiu decido devas inversigitis
       defaults:
         autofollow: Inviti al sekvi vian konton
-        avatar: Rolfiguro
+        avatar: Profilbildo
         bot: Ĉi tio estas aŭtomata konto
         chosen_languages: Filtri lingvojn
         confirm_new_password: Konfirmi novan pasvorton
@@ -223,17 +224,17 @@ eo:
         setting_boost_modal: Montri konfirman fenestron antaŭ ol diskonigi mesaĝon
         setting_default_language: Publikada lingvo
         setting_default_privacy: Privateco de afiŝado
-        setting_default_sensitive: Ĉiam marki plurmediojn kiel tiklaj
+        setting_default_sensitive: Ĉiam marki la vidaŭdaĵojn kiel tiklaj
         setting_delete_modal: Montri konfirman fenestron antaŭ ol forigi mesaĝon
         setting_disable_hover_cards: Malebligi profilan antaŭmontron kiam oni musumas
         setting_disable_swiping: Malebligi svingajn movojn
-        setting_display_media: Montrado de plurmedioj
+        setting_display_media: Vidigo de vidaŭdaĵoj
         setting_display_media_default: Implicita
         setting_display_media_hide_all: Kaŝi ĉiujn
         setting_display_media_show_all: Montri ĉiujn
         setting_expand_spoilers: Ĉiam malfoldas mesaĝojn markitajn per averto pri enhavo
         setting_hide_network: Kaŝi viajn sekvantojn kaj sekvatojn
-        setting_missing_alt_text_modal: Montru konfirman dialogon antaŭ afiŝado de amaskomunikiloj sen altteksto
+        setting_missing_alt_text_modal: Montru konfirman dialogon antaŭ afiŝado de aŭdvidaĵoj sen altteksto
         setting_reduce_motion: Redukti la movecojn de la animacioj
         setting_system_font_ui: Uzi la dekomencan tiparon de la sistemo
         setting_system_scrollbars_ui: Uzu la defaŭltan rulumilon de la sistemo
@@ -255,6 +256,7 @@ eo:
         name: Kradvorto
       filters:
         actions:
+          blur: Kaŝi amaskomunikilaron kun averto
           hide: Kaŝi komplete
           warn: Kaŝi malantaŭ averto
       form_admin_settings:
@@ -333,16 +335,22 @@ eo:
         usable: Permesi afiŝojn uzi ĉi tiun kradvorton loke
       terms_of_service:
         changelog: Kio ŝanĝiĝis?
+        effective_date: Ekvalida dato
         text: Kondiĉoj de uzado
       terms_of_service_generator:
         admin_email: Retpoŝtadreso por laŭleĝaj avizoj
         arbitration_address: Fizika adreso por arbitraciaj avizoj
         arbitration_website: Retejo por sendi arbitraciajn avizojn
+        choice_of_law: Elekto de leĝo
         dmca_address: Fizika adreso por DMCA/kopirajto-avizoj
         dmca_email: Retpoŝtadreso por DMCA/kopirajto-avizoj
         domain: Domajno
         jurisdiction: Laŭleĝa jurisdikcio
+        min_age: Minimuma aĝo
       user:
+        date_of_birth_1i: Tago
+        date_of_birth_2i: Monato
+        date_of_birth_3i: Jaro
         role: Rolo
         time_zone: Horzono
       user_role:
diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml
index d9b8e7b6a5..4333db9fed 100644
--- a/config/locales/simple_form.es-AR.yml
+++ b/config/locales/simple_form.es-AR.yml
@@ -75,6 +75,7 @@ es-AR:
       filters:
         action: Elegir qué acción realizar cuando un mensaje coincide con el filtro
         actions:
+          blur: Ocultar medios detrás de una advertencia, sin ocultar el texto en sí mismo
           hide: Ocultar completamente el contenido filtrado, comportándose como si no existiera
           warn: Ocultar el contenido filtrado detrás de una advertencia mencionando el título del filtro
       form_admin_settings:
@@ -88,6 +89,7 @@ es-AR:
         favicon: WEBP, PNG, GIF o JPG. Reemplaza el favicón predeterminado de Mastodon con uno personalizado.
         mascot: Reemplaza la ilustración en la interface web avanzada.
         media_cache_retention_period: Los archivos de medios de mensajes publicados por usuarios remotos se almacenan en la memoria caché en tu servidor. Cuando se establece un valor positivo, los medios se eliminarán después del número especificado de días. Si los datos multimedia se solicitan después de eliminarse, se volverán a descargar, si es que el contenido fuente todavía está disponible. Debido a restricciones en la frecuencia con la que las tarjetas de previsualización de enlace consultan a sitios web de terceros, se recomienda establecer este valor a, al menos, 14 días, o las tarjetas de previsualización de enlaces no se actualizarán a pedido antes de ese momento.
+        min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento durante el registro
         peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el Fediverso. Acá no se incluye ningún dato sobre si federás con un servidor determinado, sólo que tu servidor lo conoce. Esto es usado por los servicios que recopilan estadísticas sobre la federación en un sentido general.
         profile_directory: El directorio de perfiles lista a todos los usuarios que han optado a que su cuenta pueda ser descubierta.
         require_invite_text: Cuando registros aprobación manual, hacé que la solicitud de invitación "¿Por qué querés unirte?" sea obligatoria, en vez de opcional
@@ -132,17 +134,21 @@ es-AR:
         name: Sólo podés cambiar la capitalización de las letras, por ejemplo, para que sea más legible
       terms_of_service:
         changelog: Se puede estructurar con sintaxis Markdown.
+        effective_date: Un plazo razonable puede oscilar entre 10 y 30 días a partir de la fecha de notificación a tus usuarios.
         text: Se puede estructurar con sintaxis Markdown.
       terms_of_service_generator:
         admin_email: Los avisos legales incluyen devoluciones, órdenes judiciales, solicitudes de retiro y solicitudes de cumplimiento de la ley.
-        arbitration_address: Puede ser la misma que la dirección física anterior, o «N.D.» si utiliza correo electrónico
-        arbitration_website: Puede ser un formulario web, o «N.D.» si utiliza correo electrónico
+        arbitration_address: Puede ser la misma que la dirección física anterior, o «N.D.» si usa correo electrónico.
+        arbitration_website: Puede ser un formulario web, o «N.D.» si usa correo electrónico.
+        choice_of_law: Ciudad, región, territorio o declaración de las leyes de sustancia interna de las que se regirán todas y cada una de las reclamaciones.
         dmca_address: Para operadores estadounidenses, usen la dirección registrada en el Directorio de Agente Designado del DMCA. Una lista de casillas de correo está disponible bajo petición directa, usen la solicitud de exención del apartado postal del agente designado de la DMCA para enviar un correo electrónico a la Oficina de Derechos de Autor y describir que ustedes son moderadores de contenido basado en el hogar que temen venganza o represalia por sus acciones y que necesitan usar una casilla de correo para eliminar su dirección postal de la vista pública.
-        dmca_email: Puede ser el mismo correo electrónico utilizado para «Dirección de correo electrónico para avisos legales» de arriba
+        dmca_email: Puede ser el mismo correo electrónico utilizado para «Dirección de correo electrónico para avisos legales» de arriba.
         domain: Identificación única del servicio en línea que estás proporcionando.
         jurisdiction: Listá el país donde vive quien paga las facturas. Si es una empresa u otra entidad, enumerá el país donde está basada y la ciudad, región, territorio o provincia/estado, según corresponda.
+        min_age: No debería estar por debajo de la edad mínima requerida por las leyes de su jurisdicción.
       user:
         chosen_languages: Cuando estén marcados, sólo se mostrarán los mensajes en los idiomas seleccionados en las líneas temporales públicas
+        date_of_birth: Tenemos que asegurarnos de que al menos tenés %{age} años de edad para usar Mastodon. No almacenaremos esta información.
         role: El rol controla qué permisos tiene el usuario.
       user_role:
         color: Color que se utilizará para el rol a lo largo de la interface de usuario, como RGB en formato hexadecimal
@@ -233,7 +239,7 @@ es-AR:
         setting_display_media_show_all: Mostrar todo
         setting_expand_spoilers: Siempre expandir los mensajes marcados con advertencias de contenido
         setting_hide_network: Ocultá tu gráfica social
-        setting_missing_alt_text_modal: Mostrar diálogo de confirmación antes de publicar medios sin texto alternativo
+        setting_missing_alt_text_modal: Mostrar diálogo de confirmación antes de enviar medios sin texto alternativo
         setting_reduce_motion: Reducir el movimiento de las animaciones
         setting_system_font_ui: Utilizar la tipografía predeterminada del sistema
         setting_system_scrollbars_ui: Usar la barra de desplazamiento predeterminada del sistema operativo
@@ -255,6 +261,7 @@ es-AR:
         name: Etiqueta
       filters:
         actions:
+          blur: Ocultar medios con una advertencia
           hide: Ocultar completamente
           warn: Ocultar con una advertencia
       form_admin_settings:
@@ -268,6 +275,7 @@ es-AR:
         favicon: Favicón
         mascot: Mascota personalizada (legado)
         media_cache_retention_period: Período de retención de la caché de medios
+        min_age: Edad mínima requerida
         peers_api_enabled: Publicar lista de servidores descubiertos en la API
         profile_directory: Habilitar directorio de perfiles
         registrations_mode: Quién puede registrarse
@@ -333,16 +341,22 @@ es-AR:
         usable: Permitir que los mensajes usen esta etiqueta localmente
       terms_of_service:
         changelog: "¿Qué cambió?"
+        effective_date: Fecha efectiva
         text: Términos del servicio
       terms_of_service_generator:
         admin_email: Dirección de correo electrónico para avisos legales
         arbitration_address: Dirección física para avisos de arbitraje
         arbitration_website: Sitio web para enviar avisos de arbitraje
+        choice_of_law: Elección de ley
         dmca_address: Dirección física para avisos de DMCA/derechos de autor
         dmca_email: Dirección de correo electrónico para avisos de DMCA/derechos de autor
         domain: Dominio
         jurisdiction: Jurisdicción legal
+        min_age: Edad mínima
       user:
+        date_of_birth_1i: Día
+        date_of_birth_2i: Mes
+        date_of_birth_3i: Año
         role: Rol
         time_zone: Zona horaria
       user_role:
diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml
index 598ed25820..b2f7daf500 100644
--- a/config/locales/simple_form.es-MX.yml
+++ b/config/locales/simple_form.es-MX.yml
@@ -51,7 +51,7 @@ es-MX:
         inbox_url: Copia la URL de la página principal del relé que deseas usar
         irreversible: Las publicaciones filtradas desaparecerán irreversiblemente, incluso si este filtro es eliminado más adelante
         locale: El idioma de la interfaz de usuario, correos y notificaciones push
-        password: Utiliza al menos 8 caracteres
+        password: Usa al menos 8 caracteres
         phrase: Se aplicará sin importar las mayúsculas o los avisos de contenido de una publicación
         scopes: Qué APIs de la aplicación tendrán acceso. Si seleccionas el alcance de nivel mas alto, no necesitas seleccionar las individuales.
         setting_aggregate_reblogs: No mostrar nuevos impulsos para las publicaciones que han sido recientemente impulsadas (sólo afecta a las publicaciones recibidas recientemente)
@@ -68,13 +68,14 @@ es-MX:
       domain_allow:
         domain: Este dominio podrá obtener datos de este servidor y los datos entrantes serán procesados y archivados
       email_domain_block:
-        domain: Este puede ser el nombre de dominio que se muestra en al dirección de correo o el registro MX que utiliza. Se comprobarán al registrarse.
+        domain: Este puede ser el nombre de dominio que se muestra en la dirección de correo o el registro MX que utiliza. Se comprobarán al registrarse.
         with_dns_records: Se hará un intento de resolver los registros DNS del dominio dado y los resultados serán también puestos en lista negra
       featured_tag:
-        name: 'Aquí están algunas de las etiquetas que más has utilizado recientemente:'
+        name: 'Aquí están algunas de las etiquetas que más has usado recientemente:'
       filters:
         action: Elegir qué acción realizar cuando una publicación coincide con el filtro
         actions:
+          blur: Ocultar contenido multimedia detrás de una advertencia, sin ocultar el propio texto
           hide: Ocultar completamente el contenido filtrado, comportándose como si no existiera
           warn: Ocultar el contenido filtrado detrás de una advertencia mencionando el título del filtro
       form_admin_settings:
@@ -88,14 +89,15 @@ es-MX:
         favicon: WEBP, PNG, GIF o JPG. Reemplaza el icono predeterminado de Mastodon con un icono personalizado.
         mascot: Reemplaza la ilustración en la interfaz web avanzada.
         media_cache_retention_period: Los archivos multimedia de las publicaciones realizadas por usuarios remotos se almacenan en caché en su servidor. Si se establece en un valor positivo, los archivos multimedia se eliminarán tras el número de días especificado. Si los datos multimedia se solicitan después de haber sido eliminados, se volverán a descargar, si el contenido de origen sigue estando disponible. Debido a las restricciones sobre la frecuencia con la que las tarjetas de previsualización de enlaces sondean sitios de terceros, se recomienda establecer este valor en al menos 14 días, o las tarjetas de previsualización de enlaces no se actualizarán bajo demanda antes de ese tiempo.
-        peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el fediverso. Aquí no se incluye ningún dato sobre si usted federa con un servidor determinado, sólo que su servidor lo sabe. Esto es utilizado por los servicios que recopilan estadísticas sobre la federación en un sentido general.
+        min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento al registrarse
+        peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el fediverso. Aquí no se incluye ningún dato sobre si usted federa con un servidor determinado, solamente que su servidor lo sabe. Esto es usado por los servicios que recopilan estadísticas sobre la federación en un sentido general.
         profile_directory: El directorio de perfiles lista a todos los usuarios que han optado por que su cuenta pueda ser descubierta.
         require_invite_text: Cuando los registros requieren aprobación manual, hace obligatoria la entrada de texto "¿Por qué quieres unirte?" en lugar de opcional
         site_contact_email: Cómo la gente puede ponerse en contacto contigo para consultas legales o de ayuda.
         site_contact_username: Cómo puede contactarte la gente en Mastodon.
         site_extended_description: Cualquier información adicional que pueda ser útil para los visitantes y sus usuarios. Se puede estructurar con formato Markdown.
         site_short_description: Una breve descripción para ayudar a identificar su servidor de forma única. ¿Quién lo administra, a quién va dirigido?
-        site_terms: Utiliza tu propia política de privacidad o déjala en blanco para usar la predeterminada Puede estructurarse con formato Markdown.
+        site_terms: Usa tu propia política de privacidad o déjala en blanco para usar la predeterminada Puede estructurarse con formato Markdown.
         site_title: Cómo puede referirse la gente a tu servidor además de por el nombre de dominio.
         status_page_url: URL de una página donde las personas pueden ver el estado de este servidor durante una interrupción
         theme: El tema que los visitantes no registrados y los nuevos usuarios ven.
@@ -132,17 +134,21 @@ es-MX:
         name: Sólo se puede cambiar el cajón de las letras, por ejemplo, para que sea más legible
       terms_of_service:
         changelog: Puede estructurarse con la sintaxis Markdown.
+        effective_date: Un plazo razonable puede oscilar entre 10 y 30 días a partir de la fecha en que notifica a sus usuarios.
         text: Puede estructurarse con la sintaxis Markdown.
       terms_of_service_generator:
         admin_email: Las notificaciones legales incluyen contraavisos, órdenes judiciales, solicitudes de retirada y solicitudes de aplicación de la ley.
-        arbitration_address: Puede ser la misma que la dirección física anterior, o “N/A” si se utiliza el correo electrónico
-        arbitration_website: Puede ser un formulario web, o “N/A” si se utiliza el correo electrónico
+        arbitration_address: Puede ser la misma que la dirección física anterior, o "N/A" si usa correo electrónico.
+        arbitration_website: Puede ser un formulario web, o “N/A” si usa correo electrónico.
+        choice_of_law: Ciudad, región, territorio o estado de las leyes de sustancia interna de las que se regirán todas y cada una de las reclamaciones.
         dmca_address: Para los operadores de EE. UU., utilice la dirección registrada en el Directorio de Agentes Designados de la DMCA. Un listado de apartados de correos está disponible bajo petición directa, utilice la Solicitud de Renuncia de Apartado de Correos de Agente Designado de la DMCA para enviar un correo electrónico a la Oficina de Derechos de Autor y describa que usted es un moderador de contenidos desde su casa que teme venganza o represalias por sus acciones y necesita utilizar un apartado de correos para eliminar su dirección particular de la vista del público.
-        dmca_email: Puede ser la misma dirección de correo electrónico usada en “Dirección de correo electrónico para notificaciones legales”
+        dmca_email: Puede ser el mismo correo electrónico usado para “Dirección de correo electrónico para avisos legales” de arriba.
         domain: Identificación única del servicio en línea que presta.
         jurisdiction: Indique el país de residencia de quien paga las facturas. Si se trata de una empresa u otra entidad, indique el país en el que está constituida y la ciudad, región, territorio o estado, según proceda.
+        min_age: No debe ser menor de la edad mínima exigida por las leyes de su jurisdicción.
       user:
         chosen_languages: Cuando se marca, solo se mostrarán las publicaciones en los idiomas seleccionados en las líneas de tiempo públicas
+        date_of_birth: Tenemos que asegurarnos de que tienes al menos %{age} para usar Mastodon. No almacenaremos esto.
         role: El rol controla qué permisos tiene el usuario.
       user_role:
         color: Color que se usará para el rol en toda la interfaz de usuario, como RGB en formato hexadecimal
@@ -255,6 +261,7 @@ es-MX:
         name: Etiqueta
       filters:
         actions:
+          blur: Ocultar contenido multimedia con una advertencia
           hide: Ocultar completamente
           warn: Ocultar con una advertencia
       form_admin_settings:
@@ -268,6 +275,7 @@ es-MX:
         favicon: Favicon
         mascot: Mascota personalizada (legado)
         media_cache_retention_period: Período de retención de caché multimedia
+        min_age: Edad mínima requerida
         peers_api_enabled: Publicar lista de servidores descubiertos en la API
         profile_directory: Habilitar directorio de perfiles
         registrations_mode: Quién puede registrarse
@@ -333,16 +341,22 @@ es-MX:
         usable: Permitir a las publicaciones usar esta etiqueta localmente
       terms_of_service:
         changelog: "¿Qué es lo que ha cambiado?"
+        effective_date: Fecha de entrada en vigor
         text: Condiciones del servicio
       terms_of_service_generator:
         admin_email: Dirección de correo electrónico para notificaciones legales
         arbitration_address: Dirección física para notificaciones de arbitraje
         arbitration_website: Sitio web para presentar notificaciones de arbitraje
+        choice_of_law: Elección de la ley
         dmca_address: Dirección física para avisos de DMCA/derechos de autor
         dmca_email: Dirección de correo electrónico para avisos de DMCA/derechos de autor
         domain: Dominio
         jurisdiction: Jurisdicción legal
+        min_age: Edad mínima
       user:
+        date_of_birth_1i: Día
+        date_of_birth_2i: Mes
+        date_of_birth_3i: Año
         role: Rol
         time_zone: Zona horaria
       user_role:
diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml
index 9d840abadd..d469c6ec3a 100644
--- a/config/locales/simple_form.es.yml
+++ b/config/locales/simple_form.es.yml
@@ -75,6 +75,7 @@ es:
       filters:
         action: Elegir qué acción realizar cuando una publicación coincide con el filtro
         actions:
+          blur: Ocultar contenido multimedia detrás de una advertencia, sin ocultar el texto en sí
           hide: Ocultar completamente el contenido filtrado, comportándose como si no existiera
           warn: Ocultar el contenido filtrado detrás de una advertencia mencionando el título del filtro
       form_admin_settings:
@@ -88,6 +89,7 @@ es:
         favicon: WEBP, PNG, GIF o JPG. Reemplaza el favicon predeterminado de Mastodon con un icono personalizado.
         mascot: Reemplaza la ilustración en la interfaz web avanzada.
         media_cache_retention_period: Los archivos multimedia de las publicaciones creadas por usuarios remotos se almacenan en caché en tu servidor. Cuando se establece un valor positivo, estos archivos se eliminarán después del número especificado de días. Si los datos multimedia se solicitan después de eliminarse, se volverán a descargar, si el contenido fuente todavía está disponible. Debido a restricciones en la frecuencia con la que las tarjetas de previsualización de enlaces realizan peticiones a sitios de terceros, se recomienda establecer este valor a al menos 14 días, o las tarjetas de previsualización de enlaces no se actualizarán bajo demanda antes de ese momento.
+        min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento durante el registro
         peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el Fediverso. Aquí no se incluye ningún dato sobre si federas con un servidor determinado, solo que tu servidor lo conoce. Esto es utilizado por los servicios que recopilan estadísticas sobre la federación en un sentido general.
         profile_directory: El directorio de perfiles lista a todos los usuarios que han optado por que su cuenta pueda ser descubierta.
         require_invite_text: Cuando los registros requieren aprobación manual, hace obligatoria la entrada de texto "¿Por qué quieres unirte?" en lugar de opcional
@@ -132,17 +134,21 @@ es:
         name: Sólo se puede cambiar el cajón de las letras, por ejemplo, para que sea más legible
       terms_of_service:
         changelog: Se puede estructurar con sintaxis Markdown.
+        effective_date: Un plazo razonable puede oscilar entre 10 y 30 días a partir de la fecha de notificación a los usuarios.
         text: Se puede estructurar con sintaxis Markdown.
       terms_of_service_generator:
         admin_email: Los avisos legales incluyen devoluciones, órdenes judiciales, solicitudes de retiro y solicitudes de cumplimiento de la ley.
-        arbitration_address: Puede ser la misma que la dirección física anterior, o "N/A" si utiliza correo electrónico
-        arbitration_website: Puede ser un formulario web, o “N/A” si utiliza correo electrónico
+        arbitration_address: Puede ser la misma que la dirección física anterior, o "N/A" si utiliza correo electrónico.
+        arbitration_website: Puede ser un formulario web, o “N/A” si utiliza correo electrónico.
+        choice_of_law: Ciudad, región, territorio o estado de los juzgados que regirán todas y cada una de las disputas legales.
         dmca_address: Para operadores estadounidenses, utilice la dirección registrada en el DMCA Designated Agent Directory. Un listado P.O. Box está disponible bajo petición directa, use la DMCA Designated Agent Post Office Box Waiver Request para enviar un correo electrónico a la Oficina de Derechos de Autor y describir que usted es un moderador de contenido basado en el hogar que teme venganza o represalia por sus acciones y que necesita usar un P.O. Box para eliminar su dirección postal de la vista pública.
-        dmca_email: Puede ser el mismo correo electrónico utilizado para "Dirección de correo electrónico para avisos legales" de arriba
+        dmca_email: Puede ser el mismo correo electrónico utilizado para "Dirección de correo electrónico para avisos legales" de arriba.
         domain: Identificación única del servicio en línea que estás proporcionando.
         jurisdiction: Lista el país donde vive quien paga las facturas. Si es una empresa u otra entidad, enumere el país donde está basada y la ciudad, región, territorio o estado según corresponda.
+        min_age: No debería estar por debajo de la edad mínima requerida por las leyes de su jurisdicción.
       user:
         chosen_languages: Cuando se marca, solo se mostrarán las publicaciones en los idiomas seleccionados en las líneas de tiempo públicas
+        date_of_birth: Tenemos que asegurarnos de que al menos tienes %{age} años para usar Mastodon. No almacenaremos esta información.
         role: El rol controla qué permisos tiene el usuario.
       user_role:
         color: Color que se utilizará para el rol a lo largo de la interfaz de usuario, como RGB en formato hexadecimal
@@ -255,6 +261,7 @@ es:
         name: Etiqueta
       filters:
         actions:
+          blur: Ocultar contenido multimedia con una advertencia
           hide: Ocultar completamente
           warn: Ocultar con una advertencia
       form_admin_settings:
@@ -268,6 +275,7 @@ es:
         favicon: Favicon
         mascot: Mascota personalizada (legado)
         media_cache_retention_period: Período de retención de caché multimedia
+        min_age: Edad mínima requerida
         peers_api_enabled: Publicar lista de servidores descubiertos en la API
         profile_directory: Habilitar directorio de perfiles
         registrations_mode: Quién puede registrarse
@@ -333,16 +341,22 @@ es:
         usable: Permitir a las publicaciones usar esta etiqueta localmente
       terms_of_service:
         changelog: "¿Qué ha cambiado?"
+        effective_date: Fecha de entrada en vigor
         text: Términos del servicio
       terms_of_service_generator:
         admin_email: Dirección de correo electrónico para avisos legales
         arbitration_address: Dirección física para avisos de arbitraje
         arbitration_website: Sitio web para enviar avisos de arbitraje
+        choice_of_law: Legislación aplicable
         dmca_address: Dirección física para avisos de DMCA/derechos de autor
         dmca_email: Dirección de correo electrónico para avisos de DMCA/derechos de autor
         domain: Dominio
         jurisdiction: Jurisdicción legal
+        min_age: Edad mínima
       user:
+        date_of_birth_1i: Día
+        date_of_birth_2i: Mes
+        date_of_birth_3i: Año
         role: Rol
         time_zone: Zona horaria
       user_role:
diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml
index c3cf3692be..9f46cdec7d 100644
--- a/config/locales/simple_form.fa.yml
+++ b/config/locales/simple_form.fa.yml
@@ -46,11 +46,11 @@ fa:
         current_password: به دلایل امنیتی لطفاً گذرواژه این حساب را وارد کنید
         current_username: برای تأیید، لطفاً نام کاربری حساب فعلی را وارد کنید
         digest: تنها وقتی فرستاده می‌شود که مدتی طولانی فعالیتی نداشته باشید و در این مدت برای شما پیغام خصوصی‌ای نوشته شده باشد
-        email: به شما ایمیل تأییدی فرستاده خواهد شد
+        email: تأییدیه‌ای برایتان رایانامه خواهد شد
         header: یکی از قالب‌های WEBP، PNG، GIF یا JPG. بیشترین اندازه %{size}. تصویر به اندازهٔ %{dimensions} پیکسل تبدیل خواهد شد
         inbox_url: نشانی صفحهٔ اصلی رله‌ای را که می‌خواهید به کار ببرید کپی کنید
         irreversible: فرسته‌های پالوده به طور برگشت‌ناپذیری ناپدید می‌شوند، حتا اگر بعدها پالایه برداشته شود
-        locale: زبان واسط کاربری، رایانامه‌ها و آگاهی‌های ارسالی
+        locale: زبان میانای کاربری، رایانامه‌ها و آگاهی‌های ارسالی
         password: دست‌کم باید ۸ نویسه داشته باشد
         phrase: مستقل از کوچکی و بزرگی حروف، با متن اصلی یا هشدار محتوای فرسته‌ها مقایسه می‌شود
         scopes: واسط‌های برنامه‌نویسی که این برنامه به آن دسترسی دارد. اگر بالاترین سطح دسترسی را انتخاب کنید، دیگر نیازی به انتخاب سطح‌های پایینی ندارید.
@@ -132,15 +132,18 @@ fa:
         name: شما تنها می‌توانید بزرگی و کوچکی حروف را تغییر دهید تا مثلاً آن را خواناتر کنید
       terms_of_service:
         changelog: می توان با دستور Markdown ساختار داد.
+        effective_date: بازهٔ زمانی معقول می‌تواند بین ۱۰ تا ۳۰ روز از زمان آگاه کردن کاربران باشد.
         text: می توان با دستور Markdown ساختار داد.
       terms_of_service_generator:
         admin_email: اخطارهای حقوقی شامل اخطارهای متقابل، دستورها دادگاه، درخواست‌های حذف و درخواست‌های اجرای قانون است.
-        arbitration_address: می‌تواند مانند آدرس فیزیکی بالا باشد، یا در صورت استفاده از ایمیل، «N/A» باشد
-        arbitration_website: اگر از ایمیل استفاده می کنید، می تواند یک فرم وب یا "N/A" باشد
-        dmca_address: برای اپراتورهای ایالات متحده، از آدرس ثبت شده در دایرکتوری نماینده تعیین شده DMCA استفاده کنید. یک P.O. فهرست جعبه در صورت درخواست مستقیم در دسترس است، از درخواست چشم پوشی از صندوق پست دفتر پست DMCA برای ایمیل به دفتر حق نسخه برداری استفاده کنید و توضیح دهید که شما یک ناظر محتوای خانگی هستید که از انتقام یا تلافی برای اعمال خود می ترسید و باید از P.O استفاده کنید. کادری برای حذف آدرس خانه شما از نمای عمومی.
-        dmca_email: می‌تواند همان ایمیلی باشد که برای “آدرس ایمیل برای اطلاعیه‌های قانونی“ در بالا استفاده شده است
+        arbitration_address: می‌تواند همان نشانی فیزیکی بالا یا در صورت استفاده از رایانامه N/A باشد.
+        arbitration_website: می‌تواند یک فرم وب یا در صورت استفاده از رایانامه N/A باشد.
+        choice_of_law: شهر، منطقه، قلمرو یا ایالتی که قوانین ماهوی داخلی آن بر هر یک از دعاوی حاکم است.
+        dmca_address: استفاده از نشانی ثبت شده در DMCA برای کاروران ایالات متحده. سیاههٔ صندوق‌های پستی با درخواست مستقیم موجود است. استفاده از درخواست لغو صندوق پست برای رایانامه به دفتر حق رونوشت و توضیح این که یک ناظر محتوای خانگی بوده که نگران انتقام یا تلافی کنش‌هایتان هستید و نیاز به صندوق پستی برای برداشتن نشانی خانه‌تان از دید عموم دارید.
+        dmca_email: می‌تواند همان رایانامهٔ استفاده شده برای «نشانی رایانامه برای اطّلاعیه‌های حقوقی» در بالا باشد.
         domain: شناسایی منحصر به فرد سرویس آنلاینی که ارائه می کنید.
         jurisdiction: کشوری را که هر کسی که صورتحسابها را پرداخت می کند در آن زندگی می کند، فهرست کنید. اگر یک شرکت یا نهاد دیگری است، کشوری که در آن ثبت شده است و شهر، منطقه، قلمرو یا ایالت در صورت لزوم فهرست کنید.
+        min_age: نباید کم‌تر از کمینهٔ زمان لازم از سوی قوانین حقوقیتان باشد.
       user:
         chosen_languages: اگر انتخاب کنید، تنها نوشته‌هایی که به زبان‌های برگزیدهٔ شما نوشته شده‌اند در فهرست نوشته‌های عمومی نشان داده می‌شوند
         role: نقش کنترل می کند که کاربر چه مجوزهایی دارد.
@@ -172,8 +175,8 @@ fa:
         text: متن از پیش آماده‌شده
         title: عنوان
       admin_account_action:
-        include_statuses: فرسته‌های گزارش‌شده را در ایمیل بگنجان
-        send_email_notification: اطلاع‌رسانی به کاربر از راه ایمیل
+        include_statuses: قرار دادن فرسته‌های گزارش شده در رایانامه
+        send_email_notification: آگاهی کاربر به ازای هر رایانامه
         text: هشدار موردی
         type: کنش
         types:
@@ -202,7 +205,7 @@ fa:
         current_password: گذرواژه کنونی
         data: داده‌ها
         display_name: نمایش به نام
-        email: نشانی ایمیل
+        email: نشانی رایانامه
         expires_in: تاریخ انقضا
         fields: اطلاعات تکمیلی نمایه
         header: تصویر زمینه
@@ -233,6 +236,7 @@ fa:
         setting_display_media_show_all: نمایش همه
         setting_expand_spoilers: همیشه فرسته‌هایی را که هشدار محتوا دارند کامل نشان بده
         setting_hide_network: نهفتن شبکهٔ ارتباطی
+        setting_missing_alt_text_modal: نمایش گفتگوی تایید قبل از ارسال رسانه بدون متن جایگزین
         setting_reduce_motion: کاستن از حرکت در پویانمایی‌ها
         setting_system_font_ui: به‌کاربردن قلم پیش‌فرض سیستم
         setting_system_scrollbars_ui: از نوار اسکرول پیش فرض سیستم استفاده کنید
@@ -246,7 +250,7 @@ fa:
         title: عنوان
         type: نوع درون‌ریزی
         username: نام کاربری
-        username_or_email: نام کاربری یا ایمیل
+        username_or_email: نام کاربری یا رایانامه
         whole_word: تطابق واژهٔ کامل
       email_domain_block:
         with_dns_records: شامل رکوردهای MX و‌IPهای دامنه
@@ -305,12 +309,12 @@ fa:
       notification_emails:
         appeal: شخصی به تصمیم مدیر اعتراض کرد
         digest: فرستادن رایانامه‌های خلاصه
-        favourite: وقتی کسی نوشتهٔ شما را پسندید ایمیل بفرست
-        follow: وقتی کسی پیگیر شما شد ایمیل بفرست
-        follow_request: وقتی کسی درخواست پیگیری کرد ایمیل بفرست
-        mention: وقتی کسی از شما نام برد ایمیل بفرست
-        pending_account: وقتی حساب تازه‌ای نیاز به بازبینی داشت ایمیل بفرست
-        reblog: وقتی کسی فرستهٔ شما را تقویت کرد ایمیل بفرست
+        favourite: شخصی فرسته‌تان را برگزید
+        follow: شخصی پیتان گرفت
+        follow_request: شخصی خواست پیتان بگیرد
+        mention: شخصی از شما نام برد
+        pending_account: حساب تازهٔ نیازمند بررسی
+        reblog: شخصی فرسته‌تان را تقویت کرد
         report: گزارش جدیدی فرستاده شد
         software_updates:
           all: آگاهی برای همهٔ به‌روز رسانی‌ها
@@ -332,16 +336,22 @@ fa:
         usable: اجازه به فرسته‌ها برای استفتاده از این برچسب به صورت محلی
       terms_of_service:
         changelog: چه چیزی تغییر کرده است؟
+        effective_date: تاریخ اعمال
         text: شرایط خدمات
       terms_of_service_generator:
-        admin_email: آدرس ایمیل برای اطلاعیه های حقوقی
+        admin_email: نشانی رایانامه برای اطّلاعیه‌های حقوقی
         arbitration_address: آدرس فیزیکی اعلامیه های داوری
         arbitration_website: وب سایت ارسال اخطارهای داوری
+        choice_of_law: انتخاب قانون
         dmca_address: آدرس فیزیکی اعلامیه‌های DMCA/حق نسخه‌برداری
-        dmca_email: آدرس ایمیل برای اعلامیه‌های DMCA/حق نسخه‌برداری
+        dmca_email: نشانی رایانامه برای اطّلاعیه‌های حق رونوشت
         domain: دامنه
         jurisdiction: صلاحیت قانونی
+        min_age: کمینهٔ زمان
       user:
+        date_of_birth_1i: روز
+        date_of_birth_2i: ماه
+        date_of_birth_3i: سال
         role: نقش
         time_zone: منطقهٔ زمانی
       user_role:
diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml
index 83b32636dc..5489780ec7 100644
--- a/config/locales/simple_form.fi.yml
+++ b/config/locales/simple_form.fi.yml
@@ -9,7 +9,7 @@ fi:
         fields: Verkkosivustosi, pronominisi, ikäsi ja mitä ikinä haluatkaan ilmoittaa.
         indexable: Julkiset julkaisusi voivat näkyä Mastodonin hakutuloksissa. Käyttäjät, jotka ovat olleet vuorovaikutuksessa julkaisujesi kanssa, voivat etsiä niitä asetuksesta riippumatta.
         note: 'Voit @mainita muita käyttäjiä tai #aihetunnisteita.'
-        show_collections: Käyttäjät voivat selata seurattujasi ja seuraajiasi. Käyttäjät, joita seuraat, näkevät joka tapauksessa, että seuraat heitä.
+        show_collections: Käyttäjät voivat selata seurattaviasi ja seuraajiasi. Käyttäjät, joita seuraat, näkevät joka tapauksessa, että seuraat heitä.
         unlocked: Käyttäjät voivat seurata sinua pyytämättä hyväksyntääsi. Poista valinta, jos haluat tarkistaa sekä hyväksyä tai hylätä vastaanottamasi seurantapyynnöt.
       account_alias:
         acct: Määrittele sen tilin käyttäjänimi@verkkotunnus, josta haluat muuttaa
@@ -75,6 +75,7 @@ fi:
       filters:
         action: Valitse, mikä toiminto suoritetaan, kun julkaisu vastaa suodatinta
         actions:
+          blur: Piilota media varoituksen taakse piilottamatta itse tekstiä
           hide: Piilota suodatettu sisältö kokonaan, ikään kuin sitä ei olisi olemassa
           warn: Piilota suodatettu sisältö varoituksen taakse, jossa mainitaan suodattimen nimi
       form_admin_settings:
@@ -88,6 +89,7 @@ fi:
         favicon: WEBP, PNG, GIF tai JPG. Korvaa oletusarvoisen Mastodonin sivustokuvakkeen haluamallasi kuvakkeella.
         mascot: Korvaa kuvituksen edistyneessä selainkäyttöliittymässä.
         media_cache_retention_period: Etäkäyttäjien tekemien julkaisujen mediatiedostot ovat välimuistissa palvelimellasi. Kun kentän arvo on positiivinen, media poistuu, kun määritetty määrä päiviä on kulunut. Jos mediaa pyydetään sen poistamisen jälkeen, se ladataan uudelleen, jos lähdesisältö on vielä saatavilla. Koska linkkien esikatselun kyselyitä kolmansien osapuolien sivustoille on rajoitettu, on suositeltavaa asettaa tämä arvo vähintään 14 päivään, tai linkkien kortteja ei päivitetä pyynnöstä ennen tätä ajankohtaa.
+        min_age: Käyttäjiä pyydetään rekisteröitymisen aikana vahvistamaan syntymäpäivänsä
         peers_api_enabled: Luettelo verkkotunnuksista, jotka tämä palvelin on kohdannut fediversumissa. Se ei kerro, federoitko tietyn palvelimen kanssa, vaan että palvelimesi on ylipäätään tietoinen siitä. Tätä tietoa käytetään palveluissa, jotka keräävät tilastoja federoinnista yleisellä tasolla.
         profile_directory: Profiilihakemisto luetteloi kaikki käyttäjät, jotka ovat valinneet olla löydettävissä.
         require_invite_text: Kun rekisteröityminen vaatii manuaalisen hyväksynnän, tee ”Miksi haluat liittyä?” -tekstikentästä pakollinen vapaaehtoisen sijaan
@@ -132,17 +134,20 @@ fi:
         name: Voit esimerkiksi vaihtaa suur- ja pienaakkosten kesken helppolukuistaaksesi tekstiäsi
       terms_of_service:
         changelog: Voidaan jäsentää Markdown-syntaksilla.
+        effective_date: Sopiva aika on 10–30 päivää siitä, kun olet ilmoittanut ehdoista käyttäjillesi.
         text: Voidaan jäsentää Markdown-syntaksilla.
       terms_of_service_generator:
         admin_email: Oikeudellisiin ilmoituksiin kuuluvat vastailmoitukset, oikeuden määräykset, poistopyynnöt ja lainvalvontaviranomaisten pyynnöt.
-        arbitration_address: Voi olla sama kuin edellä oleva fyysinen osoite tai ”N/A”, jos käytät sähköpostia
-        arbitration_website: Voi olla verkkolomake tai ”N/A”, jos käytät sähköpostia
+        arbitration_address: Voi olla sama kuin edellä mainittu Fyysinen osoite tai ”N/A”, jos käytät sähköpostia.
+        arbitration_website: Voi olla verkkolomake tai ”N/A”, jos käytät sähköpostia.
         dmca_address: Yhdysvaltalaisten operaattoreiden on käytettävä DMCA Designated Agent Directory -luetteloon rekisteröityä osoitetta. Postilokeroluettelo on saatavissa suoralla pyynnöllä, joten käytä DMCA Designated Agent Post Office Box Waiver Request -lomaketta lähettääksesi sähköpostia tekijänoikeusvirastolle ja kuvaile, että olet kotona toimiva sisältömoderaattori, joka pelkää kostoa tai rangaistusta toimistaan ja tarvitsee postilokeroa pitääkseen kotiosoitteensa poissa julkisuudesta.
-        dmca_email: Voi olla sama sähköpostiosoite kuin edellä kohdassa ”Sähköpostiosoite oikeudellisia ilmoituksia varten”
+        dmca_email: Voi olla sama kuin edellä mainittu ”Sähköpostiosoite oikeudellisille ilmoituksille”.
         domain: Tarjoamasi verkkopalvelun yksilöllinen tunniste.
         jurisdiction: Mainitse valtio, jossa laskujen maksaja asuu. Jos kyseessä on yritys tai muu yhteisö, mainitse valtio, johon se on rekisteröity, ja tarvittaessa kaupunki, alue, territorio tai osavaltio.
+        min_age: Ei pidä alittaa lainkäyttöalueesi lakien vaatimaa vähimmäisikää.
       user:
         chosen_languages: Jos valitset kieliä oheisesta luettelosta, vain niidenkieliset julkaisut näkyvät sinulle julkisilla aikajanoilla
+        date_of_birth: Meidän on varmistettava, että olet vähintään %{age} vuotta vanha, jotta voit käyttää Mastodonia. Emme tallenna tätä.
         role: Rooli määrää, millaiset käyttöoikeudet käyttäjällä on.
       user_role:
         color: Väri, jota käytetään roolille kaikkialla käyttöliittymässä, RGB-heksadesimaalimuodossa
@@ -162,7 +167,7 @@ fi:
           name: Nimike
           value: Sisältö
         indexable: Sisällytä julkiset julkaisut hakutuloksiin
-        show_collections: Näytä seuratut ja seuraajat profiilissa
+        show_collections: Näytä seurattavat ja seuraajat profiilissa
         unlocked: Hyväksy uudet seuraajat automaattisesti
       account_alias:
         acct: Vanhan tilin käyttäjätunnus
@@ -255,6 +260,7 @@ fi:
         name: Aihetunniste
       filters:
         actions:
+          blur: Piilota media varoittaen
           hide: Piilota kokonaan
           warn: Piilota varoittaen
       form_admin_settings:
@@ -268,6 +274,7 @@ fi:
         favicon: Sivustokuvake
         mascot: Mukautettu maskotti (vanhentunut)
         media_cache_retention_period: Mediasisällön välimuistin säilytysaika
+        min_age: Vähimmäisikävaatimus
         peers_api_enabled: Julkaise löydettyjen palvelinten luettelo ohjelmointirajapinnassa
         profile_directory: Ota profiilihakemisto käyttöön
         registrations_mode: Kuka voi rekisteröityä
@@ -333,6 +340,7 @@ fi:
         usable: Salli julkaisujen käyttää tätä aihetunnistetta paikallisesti
       terms_of_service:
         changelog: Mikä on muuttunut?
+        effective_date: Voimaantulopäivä
         text: Käyttöehdot
       terms_of_service_generator:
         admin_email: Sähköpostiosoite oikeudellisille ilmoituksille
@@ -342,7 +350,11 @@ fi:
         dmca_email: Sähköpostiosoite DMCA-/tekijänoikeusilmoituksille
         domain: Verkkotunnus
         jurisdiction: Lainkäyttöalue
+        min_age: Vähimmäisikä
       user:
+        date_of_birth_1i: Päivä
+        date_of_birth_2i: Kuukausi
+        date_of_birth_3i: Vuosi
         role: Rooli
         time_zone: Aikavyöhyke
       user_role:
diff --git a/config/locales/simple_form.fo.yml b/config/locales/simple_form.fo.yml
index aaa1cbdeee..32d066ed18 100644
--- a/config/locales/simple_form.fo.yml
+++ b/config/locales/simple_form.fo.yml
@@ -75,6 +75,7 @@ fo:
       filters:
         action: Vel, hvat skal henda, tá eitt uppslag svarar til filtrið
         actions:
+          blur: Fjal miðlar aftanfyri eina ávaring, uttan at fjala sjálvan tekstin
           hide: Fjal filtreraða innihaldið fullkomiliga, ber seg at sum at tað ikki fanst
           warn: Fjal filtreraða innihaldið aftan fyri eina ávaring, sum nevnir heitið á filtrinum
       form_admin_settings:
@@ -88,6 +89,7 @@ fo:
         favicon: WEBP, PNG, GIF ella JPG. Býtir vanligu Mastodon fav-ikonina um við eina ser-ikon.
         mascot: Skúgvar til viks myndprýðingina í framkomna vev-markamótinum.
         media_cache_retention_period: Miðlafílur frá postum, sum fjarbrúkarar hava gjørt, verða goymdir á tínum ambætara. Tá hetta er sett til eitt virði størri enn 0, so verða miðlafílurnar strikaðar eftir ásetta talið av døgum. Um miðladátur verða umbidnar eftir at tær eru strikaðar, verða tær tiknar innaftur á ambætaran, um keldutilfarið enn er tøkt. Vegna avmarkingar á hvussu ofta undanvísingarkort til leinki spyrja triðjapartsstøð, so verður mælt til at seta hetta virðið til í minsta lagi 14 dagar. Annars verða umbønir um dagføringar av undanvísingarkortum til leinki ikki gjørdar áðrenn hetta.
+        min_age: Brúkarar verða spurdir um at vátta teirra føðingardag, tá tey skráseta seg
         peers_api_enabled: Ein listi við navnaøkjum, sum hesin ambætarin er komin framat í fediversinum. Ongar dátur eru tiknar við her um tú er sameind/ur við ein givnan ambætara, einans at tín ambætari veit um hann. Hetta verður brúkt av tænastum, sum gera hagtøl um sameining yvirhøvur.
         profile_directory: Vangaskráin listar allar brúkarar, sum hava valt at kunna uppdagast.
         require_invite_text: Tá tilmeldingar krevja serskilda góðkenning, set so "Hví vil tú vera við?" tekstateigin til at vera kravdan heldur enn valfrían
@@ -132,17 +134,21 @@ fo:
         name: Tú kanst einans broyta millum stórar og smáar stavir, til dømis fyri at gera tað meira lesiligt
       terms_of_service:
         changelog: Kunnu vera uppbygdar við Markdown syntaksi.
+        effective_date: Ein rímulig tíðarfreist kann vera alt millum 10 og 30 dagar frá degnum, tú gevur boð til brúkararnar hjá tær.
         text: Kunnu vera uppbygdar við Markdown syntaksi.
       terms_of_service_generator:
         admin_email: Løgfrøðisligar fráboðanir fata um rættarligar mótfráboðanir, úrskurðir, áheitanir um at taka niður og løgfrøðisligar áheitanir.
-        arbitration_address: Kann vera tann sami sum fysiski bústaðurin omanfyri ella "N/A", um teldupostur verður brúktur
-        arbitration_website: Kann vera ein vevformularur ella "N/A", um teldupostur verður brúktur
+        arbitration_address: Kann vera tann sami sum fysiski bústaðurin omanfyri ella "N/A", um teldupostur verður brúktur.
+        arbitration_website: Kann vera ein vevformularur ella "N/A", um teldupostur verður brúktur.
+        choice_of_law: Býur, øki, landspartur ella statur, hvørs innanhýsis lógir skulu stýra øllum krøvum.
         dmca_address: Fyristøðufólk og -feløg í USA brúka bústaðin, ið er skrásettur í DMCA Designated Agent Directory. Ein postsmoguskráseting er tøk, um biðið verður um hana beinleiðis. Brúka DMCA Designated Agent Post Office Box Waiver Request fyri at senda teldubræv til Copyright Office og greið frá, at tú er ein kjakleiðari, sum virkar heimanifrá, og at tú er bangin fyri hevnd ella afturløning fyri tí, tú ger, og at tú hevur tørv á at brúka eina postsmogu fyri at fjala heimabústaðin fyri almenninginum.
-        dmca_email: Kann vera sami teldupoststaður, sum er brúktur til "Teldupoststaður fyri løgfrøðisligar fráboðanir" omanfyri
+        dmca_email: Kann vera sami teldupoststaður, sum er brúktur til "Teldupoststaður fyri løgfrøðisligar fráboðanir" omanfyri.
         domain: Makaleys eyðmerking av nettænastuni, sum tú veitir.
         jurisdiction: Lista landið, har sum tann, ið rindar rokningarnar, livir. Er tað eitt felag ella ein onnur eind, lista landið, har tað er skrásett, umframt býin, økið, umveldið ella statin, alt eftir hvat er hóskandi.
+        min_age: Eigur ikki at vera undir lægsta aldri, sum lógirnar í tínum rættarøki krevja.
       user:
         chosen_languages: Tá hetta er valt, verða einans postar í valdum málum vístir á almennum tíðarlinjum
+        date_of_birth: Vit mugu tryggja okkum, at tú er í minsta lagi %{age} ár fyri at brúka Mastodon. Vit goyma ikki hesar upplýsingar.
         role: Leikluturin stýrir hvørji rættindi, brúkarin hevur.
       user_role:
         color: Litur, sum leikluturin hevur í øllum brúkaramarkamótinum, sum RGB og upplýst sum sekstandatal
@@ -255,6 +261,7 @@ fo:
         name: Tvíkrossur
       filters:
         actions:
+          blur: Fjal miðlar við eini ávaring
           hide: Fjal fullkomiliga
           warn: Fjal við eini ávaring
       form_admin_settings:
@@ -268,6 +275,7 @@ fo:
         favicon: Favikon
         mascot: Serskildur maskottur (arvur)
         media_cache_retention_period: Tíðarskeið, har miðlagoymslur verða varðveittar
+        min_age: Aldursmark
         peers_api_enabled: Kunnger lista við uppdagaðum ambætarum í API'num
         profile_directory: Ger vangaskrá virkna
         registrations_mode: Hvør kann tilmelda seg
@@ -333,16 +341,22 @@ fo:
         usable: Loyv postum at brúka hetta frámerki lokalt
       terms_of_service:
         changelog: Hvat er broytt?
+        effective_date: Galdandi frá
         text: Tænastutreytir
       terms_of_service_generator:
         admin_email: Teldupoststaður fyri løgfrøðisligar fráboðanir
         arbitration_address: Fysisk adressa fyri gerðarrættarfráboðanir
         arbitration_website: Heimasíða har gerðarrættarfráboðanir kunnu innlatast
+        choice_of_law: Val av lóg
         dmca_address: Fysiskur bústaður fyri DMCA/copyright fráboðanir
         dmca_email: Teldubústaður fyri DMCA/copyright fráboðanir
         domain: Navnaøki
         jurisdiction: Løgdømi
+        min_age: Lægsti aldur
       user:
+        date_of_birth_1i: Dagur
+        date_of_birth_2i: Mánaði
+        date_of_birth_3i: Ár
         role: Leiklutur
         time_zone: Tíðarsona
       user_role:
diff --git a/config/locales/simple_form.fr-CA.yml b/config/locales/simple_form.fr-CA.yml
index d229808eae..f917183f55 100644
--- a/config/locales/simple_form.fr-CA.yml
+++ b/config/locales/simple_form.fr-CA.yml
@@ -75,6 +75,7 @@ fr-CA:
       filters:
         action: Choisir l'action à effectuer quand un message correspond au filtre
         actions:
+          blur: Cacher les médias derrière un avertissement, sans cacher le texte
           hide: Cacher complètement le contenu filtré, faire comme s'il n'existait pas
           warn: Cacher le contenu filtré derrière un avertissement mentionnant le nom du filtre
       form_admin_settings:
@@ -88,6 +89,7 @@ fr-CA:
         favicon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
         mascot: Remplace l'illustration dans l'interface Web avancée.
         media_cache_retention_period: Les fichiers médias des messages publiés par des utilisateurs distants sont mis en cache sur votre serveur. Lorsque cette valeur est positive, les médias sont supprimés au terme du nombre de jours spécifié. Si les données des médias sont demandées après leur suppression, elles seront téléchargées à nouveau, dans la mesure où le contenu source est toujours disponible. En raison des restrictions concernant la fréquence à laquelle les cartes de prévisualisation des liens interrogent des sites tiers, il est recommandé de fixer cette valeur à au moins 14 jours, faute de quoi les cartes de prévisualisation des liens ne seront pas mises à jour à la demande avant cette échéance.
+        min_age: Les utilisateurs seront invités à confirmer leur date de naissance lors de l'inscription
         peers_api_enabled: Une liste de noms de domaine que ce serveur a rencontrés dans le fédiverse. Aucune donnée indiquant si vous vous fédérez ou non avec un serveur particulier n'est incluse ici, seulement l'information que votre serveur connaît un autre serveur. Cette option est utilisée par les services qui collectent des statistiques sur la fédération en général.
         profile_directory: L'annuaire des profils répertorie tous les utilisateurs qui ont opté pour être découverts.
         require_invite_text: Lorsque les inscriptions nécessitent une approbation manuelle, rendre le texte de l’invitation "Pourquoi voulez-vous vous inscrire ?" obligatoire plutôt que facultatif
@@ -132,17 +134,21 @@ fr-CA:
         name: Vous ne pouvez modifier que la casse des lettres, par exemple, pour le rendre plus lisible
       terms_of_service:
         changelog: Peut être structuré avec la syntaxe Markdown.
+        effective_date: Un délai raisonnable peut varier entre 10 et 30 jours à compter de la date à laquelle vous informez vos utilisateurs.
         text: Peut être structuré avec la syntaxe Markdown.
       terms_of_service_generator:
         admin_email: Les avis juridiques comprennent les contre-avis, les ordonnances judiciaires, les demandes de retrait et les demandes des forces de l'ordre.
-        arbitration_address: Peut être la même que l'adresse physique ci-dessus, ou "N/A" si vous utilisez un e-mail
-        arbitration_website: Peut être un formulaire web, ou « N/A» si vous utilisez un e-mail
+        arbitration_address: Il peut s'agir de la même que l'adresse physique ci-dessus, ou « N/A » si vous utilisez une adresse e-mail.
+        arbitration_website: Il peut s'agir d'un formulaire web ou de « N/A » s'il s'agit d'un courrier électronique.
+        choice_of_law: Ville, région, territoire ou État dont le droit matériel interne régit toute réclamation.
         dmca_address: Pour les opérateurs américains, utilisez l'adresse enregistrée dans le répertoire des agents désignés du DMCA Designated Agent Directory. Une boîte postale est disponible sur demande directe. Utilisez le formulaire de demande de dérogation pour l'utilisation d'une boîte postale par un agent désigné du Designated Agent Post Office Box Waiver Request pour envoyer un e-mail au Bureau du droit d'auteur (Copyright Office) et expliquer que vous êtes un modérateur de contenu à domicile qui craint des représailles ou une vengeance pour ses actions et que vous avez besoin d'utiliser une boîte postale afin de masquer votre adresse personnelle au public.
-        dmca_email: Peut être la même adresse e-mail que celle utilisée pour « Adresse e-mail pour les mentions légales » ci-dessus
+        dmca_email: Il peut s'agir du même courriel que celui utilisé pour l'« Adresse électronique pour les avis juridiques » ci-dessus.
         domain: Identification unique du service en ligne que vous offrez.
         jurisdiction: Indiquez le pays dans lequel réside la personne qui paie les factures. S'il s'agit d'une entreprise ou d'une autre entité, indiquez le pays dans lequel elle est enregistrée, ainsi que la ville, la région, le territoire ou l'État, le cas échéant.
+        min_age: Ne doit pas être en dessous de l’âge minimum requis par les lois de votre juridiction.
       user:
         chosen_languages: Lorsque coché, seuls les messages dans les langues sélectionnées seront affichés sur les fils publics
+        date_of_birth: Nous devons nous assurer que vous avez au moins %{age} pour utiliser Mastodon. Nous ne conserverons pas ces informations.
         role: Le rôle définit quelles autorisations a l'utilisateur⋅rice.
       user_role:
         color: Couleur à attribuer au rôle dans l'interface, au format hexadécimal RVB
@@ -233,6 +239,7 @@ fr-CA:
         setting_display_media_show_all: Montrer tout
         setting_expand_spoilers: Toujours déplier les messages marqués d’un avertissement de contenu
         setting_hide_network: Cacher votre réseau
+        setting_missing_alt_text_modal: Afficher une fenêtre de confirmation avant de poster un média sans texte alternatif
         setting_reduce_motion: Réduire la vitesse des animations
         setting_system_font_ui: Utiliser la police par défaut du système
         setting_system_scrollbars_ui: Utiliser la barre de défilement par défaut du système
@@ -254,6 +261,7 @@ fr-CA:
         name: Mot-clic
       filters:
         actions:
+          blur: Masquer les médias derrière un avertissement
           hide: Cacher complètement
           warn: Cacher derrière un avertissement
       form_admin_settings:
@@ -267,6 +275,7 @@ fr-CA:
         favicon: Favicon
         mascot: Mascotte personnalisée (héritée)
         media_cache_retention_period: Durée de rétention des médias dans le cache
+        min_age: Âge minimum requis
         peers_api_enabled: Publie la liste des serveurs découverts dans l'API
         profile_directory: Activer l’annuaire des profils
         registrations_mode: Qui peut s’inscrire
@@ -332,16 +341,22 @@ fr-CA:
         usable: Autoriser les messages à utiliser ce hashtag localement
       terms_of_service:
         changelog: Nouveautés?
+        effective_date: Date effective
         text: Conditions d'utilisation
       terms_of_service_generator:
         admin_email: Adresse électronique pour les notifications légales
         arbitration_address: Adresse physique pour les notifications d'arbitrage
         arbitration_website: Site Web pour soumettre les notifications d'arbitrage
+        choice_of_law: Choix de la loi
         dmca_address: Adresse physique pour les avis DMCA/copyright
         dmca_email: Adresse e-mail pour les avis DMCA/copyright
         domain: Domaine
         jurisdiction: Juridiction
+        min_age: Âge minimum
       user:
+        date_of_birth_1i: Jour
+        date_of_birth_2i: Mois
+        date_of_birth_3i: Année
         role: Rôle
         time_zone: Fuseau horaire
       user_role:
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index 4fa202847b..a8d6bb86e0 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -75,6 +75,7 @@ fr:
       filters:
         action: Choisir l'action à effectuer quand un message correspond au filtre
         actions:
+          blur: Cacher les médias derrière un avertissement, sans cacher le texte
           hide: Cacher complètement le contenu filtré, faire comme s'il n'existait pas
           warn: Cacher le contenu filtré derrière un avertissement mentionnant le nom du filtre
       form_admin_settings:
@@ -88,6 +89,7 @@ fr:
         favicon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
         mascot: Remplace l'illustration dans l'interface Web avancée.
         media_cache_retention_period: Les fichiers médias des messages publiés par des utilisateurs distants sont mis en cache sur votre serveur. Lorsque cette valeur est positive, les médias sont supprimés au terme du nombre de jours spécifié. Si les données des médias sont demandées après leur suppression, elles seront téléchargées à nouveau, dans la mesure où le contenu source est toujours disponible. En raison des restrictions concernant la fréquence à laquelle les cartes de prévisualisation des liens interrogent des sites tiers, il est recommandé de fixer cette valeur à au moins 14 jours, faute de quoi les cartes de prévisualisation des liens ne seront pas mises à jour à la demande avant cette échéance.
+        min_age: Les utilisateurs seront invités à confirmer leur date de naissance lors de l'inscription
         peers_api_enabled: Une liste de noms de domaine que ce serveur a rencontrés dans le fédiverse. Aucune donnée indiquant si vous vous fédérez ou non avec un serveur particulier n'est incluse ici, seulement l'information que votre serveur connaît un autre serveur. Cette option est utilisée par les services qui collectent des statistiques sur la fédération en général.
         profile_directory: L'annuaire des profils répertorie tous les comptes qui choisi d'être découvrables.
         require_invite_text: Lorsque les inscriptions nécessitent une approbation manuelle, rendre le texte de l’invitation "Pourquoi voulez-vous vous inscrire ?" obligatoire plutôt que facultatif
@@ -132,17 +134,21 @@ fr:
         name: Vous ne pouvez modifier que la casse des lettres, par exemple, pour le rendre plus lisible
       terms_of_service:
         changelog: Peut être structuré avec la syntaxe Markdown.
+        effective_date: Un délai raisonnable peut varier entre 10 et 30 jours à compter de la date à laquelle vous informez vos utilisateurs.
         text: Peut être structuré avec la syntaxe Markdown.
       terms_of_service_generator:
         admin_email: Les avis juridiques comprennent les contre-avis, les ordonnances judiciaires, les demandes de retrait et les demandes des forces de l'ordre.
-        arbitration_address: Peut être la même que l'adresse physique ci-dessus, ou "N/A" si vous utilisez un e-mail
-        arbitration_website: Peut être un formulaire web, ou « N/A» si vous utilisez un e-mail
+        arbitration_address: Il peut s'agir de la même que l'adresse physique ci-dessus, ou « N/A » si vous utilisez une adresse e-mail.
+        arbitration_website: Il peut s'agir d'un formulaire web ou de « N/A » s'il s'agit d'un courrier électronique.
+        choice_of_law: Ville, région, territoire ou État dont le droit matériel interne régit toute réclamation.
         dmca_address: Pour les opérateurs américains, utilisez l'adresse enregistrée dans le répertoire des agents désignés du DMCA Designated Agent Directory. Une boîte postale est disponible sur demande directe. Utilisez le formulaire de demande de dérogation pour l'utilisation d'une boîte postale par un agent désigné du Designated Agent Post Office Box Waiver Request pour envoyer un e-mail au Bureau du droit d'auteur (Copyright Office) et expliquer que vous êtes un modérateur de contenu à domicile qui craint des représailles ou une vengeance pour ses actions et que vous avez besoin d'utiliser une boîte postale afin de masquer votre adresse personnelle au public.
-        dmca_email: Peut être la même adresse e-mail que celle utilisée pour « Adresse e-mail pour les mentions légales » ci-dessus
+        dmca_email: Il peut s'agir du même courriel que celui utilisé pour l'« Adresse électronique pour les avis juridiques » ci-dessus.
         domain: Identification unique du service en ligne que vous offrez.
         jurisdiction: Indiquez le pays dans lequel réside la personne qui paie les factures. S'il s'agit d'une entreprise ou d'une autre entité, indiquez le pays dans lequel elle est enregistrée, ainsi que la ville, la région, le territoire ou l'État, le cas échéant.
+        min_age: Ne doit pas être en dessous de l’âge minimum requis par les lois de votre juridiction.
       user:
         chosen_languages: Lorsque coché, seuls les messages dans les langues sélectionnées seront affichés sur les fils publics
+        date_of_birth: Nous devons nous assurer que vous avez au moins %{age} pour utiliser Mastodon. Nous ne conserverons pas ces informations.
         role: Le rôle définit quelles autorisations a l'utilisateur⋅rice.
       user_role:
         color: Couleur à attribuer au rôle dans l'interface, au format hexadécimal RVB
@@ -233,6 +239,7 @@ fr:
         setting_display_media_show_all: Montrer tout
         setting_expand_spoilers: Toujours déplier les messages marqués d’un avertissement de contenu
         setting_hide_network: Cacher votre réseau
+        setting_missing_alt_text_modal: Afficher une fenêtre de confirmation avant de poster un média sans texte alternatif
         setting_reduce_motion: Réduire la vitesse des animations
         setting_system_font_ui: Utiliser la police par défaut du système
         setting_system_scrollbars_ui: Utiliser la barre de défilement par défaut du système
@@ -254,6 +261,7 @@ fr:
         name: Hashtag
       filters:
         actions:
+          blur: Masquer les médias derrière un avertissement
           hide: Cacher complètement
           warn: Cacher derrière un avertissement
       form_admin_settings:
@@ -267,6 +275,7 @@ fr:
         favicon: Favicon
         mascot: Mascotte personnalisée (héritée)
         media_cache_retention_period: Durée de rétention des médias dans le cache
+        min_age: Âge minimum requis
         peers_api_enabled: Publie la liste des serveurs découverts dans l'API
         profile_directory: Activer l’annuaire des profils
         registrations_mode: Qui peut s’inscrire
@@ -332,16 +341,22 @@ fr:
         usable: Autoriser les messages à utiliser ce hashtag localement
       terms_of_service:
         changelog: Nouveautés?
+        effective_date: Date effective
         text: Conditions d'utilisation
       terms_of_service_generator:
         admin_email: Adresse électronique pour les notifications légales
         arbitration_address: Adresse physique pour les notifications d'arbitrage
         arbitration_website: Site Web pour soumettre les notifications d'arbitrage
+        choice_of_law: Choix de la loi
         dmca_address: Adresse physique pour les avis DMCA/copyright
         dmca_email: Adresse e-mail pour les avis DMCA/copyright
         domain: Domaine
         jurisdiction: Juridiction
+        min_age: Âge minimum
       user:
+        date_of_birth_1i: Jour
+        date_of_birth_2i: Mois
+        date_of_birth_3i: Année
         role: Rôle
         time_zone: Fuseau horaire
       user_role:
diff --git a/config/locales/simple_form.fy.yml b/config/locales/simple_form.fy.yml
index ffe2f49ef4..56f02a1ff4 100644
--- a/config/locales/simple_form.fy.yml
+++ b/config/locales/simple_form.fy.yml
@@ -135,10 +135,7 @@ fy:
         text: Kin strukturearre wurde mei Markdown-syntaksis.
       terms_of_service_generator:
         admin_email: Juridyske meidielingen binne û.o. tsjinoantekeningen, rjochterlike befelen, ferwiderfersiken en fersiken om wetshanthaving.
-        arbitration_address: Kin itselde wêze as boppesteande strjitteof ‘N/A’ by gebrûk fan e-mail
-        arbitration_website: Kin in webformulier wêze, of ‘N/A’ as e-mail brûkt wurdt
         dmca_address: 'Brûk foar behearders yn de FS: it adres dat registrearre is yn de DMCA Designated Agent Directory. Op fersyk is der in postbuslist beskikber. Brûk it DMCA Designated Agent Post Office Box Waiver Request om it Copyright Office te e-mailen en te beskriuwen dat jo in thúsbasearre ynhâldsmoderator binne dy’t wraak of fergelding frezet foar jo dieden en in postbus brûke moat om jo húsadres út it publike domein te hâlden.'
-        dmca_email: Kin itselde e-mailadres wêze as ‘E-mailadres foar juridyske berjochten’ hjirboppe
         domain: In unike identifikaasje fan de online tsjinst dy’t ferliend wurdt.
         jurisdiction: Fermeld it lân wêr’t de persoan wennet dy’t de rekkeningen betellet. As it in bedriuw of in oare entiteit is, fermeld it lân wêr’t it opnommen is en de stêd, regio, grûngebiet of steat, foar safier fan tapassing.
       user:
diff --git a/config/locales/simple_form.ga.yml b/config/locales/simple_form.ga.yml
index 93eff4a89d..bd8dac2a36 100644
--- a/config/locales/simple_form.ga.yml
+++ b/config/locales/simple_form.ga.yml
@@ -3,6 +3,7 @@ ga:
   simple_form:
     hints:
       account:
+        attribution_domains: Ceann in aghaidh an líne. Cosnaíonn sé ó sannadh bréagach.
         discoverable: Seans go mbeidh do phostálacha poiblí agus do phróifíl le feiceáil nó molta i réimsí éagsúla de Mastodon agus is féidir do phróifíl a mholadh d’úsáideoirí eile.
         display_name: D'ainm iomlán nó d'ainm spraoi.
         fields: Do leathanach baile, forainmneacha, aois, rud ar bith is mian leat.
@@ -74,6 +75,7 @@ ga:
       filters:
         action: Roghnaigh an gníomh ba cheart a dhéanamh nuair a mheaitseálann postáil an scagaire
         actions:
+          blur: Folaigh na meáin taobh thiar de rabhadh, gan an téacs féin a cheilt
           hide: Cuir an t-ábhar scagtha i bhfolach go hiomlán, ag iompar amhail is nach raibh sé ann
           warn: Folaigh an t-ábhar scagtha taobh thiar de rabhadh a luann teideal an scagaire
       form_admin_settings:
@@ -87,6 +89,7 @@ ga:
         favicon: WEBP, PNG, GIF nó JPG. Sáraíonn sé an favicon Mastodon réamhshocraithe le deilbhín saincheaptha.
         mascot: Sáraíonn sé an léaráid san ardchomhéadan gréasáin.
         media_cache_retention_period: Déantar comhaid meán ó phoist a dhéanann cianúsáideoirí a thaisceadh ar do fhreastalaí. Nuair a bheidh luach dearfach socraithe, scriosfar na meáin tar éis an líon sonraithe laethanta. Má iarrtar na sonraí meán tar éis é a scriosadh, déanfar é a ath-íoslódáil, má tá an t-ábhar foinse fós ar fáil. Mar gheall ar shrianta ar cé chomh minic is atá cártaí réamhamhairc ag vótaíocht do shuíomhanna tríú páirtí, moltar an luach seo a shocrú go 14 lá ar a laghad, nó ní dhéanfar cártaí réamhamhairc naisc a nuashonrú ar éileamh roimh an am sin.
+        min_age: Iarrfar ar úsáideoirí a ndáta breithe a dhearbhú le linn clárúcháin
         peers_api_enabled: Liosta de na hainmneacha fearainn ar tháinig an freastalaí seo orthu sa choinbhleacht. Níl aon sonraí san áireamh anseo faoi cé acu an ndéanann tú cónascadh le freastalaí ar leith, díreach go bhfuil a fhios ag do fhreastalaí faoi. Úsáideann seirbhísí a bhailíonn staitisticí ar chónaidhm go ginearálta é seo.
         profile_directory: Liostaíonn an t-eolaire próifíle na húsáideoirí go léir a roghnaigh isteach le bheith in-aimsithe.
         require_invite_text: Nuair a bhíonn faomhadh láimhe ag teastáil le haghaidh clárúcháin, déan an "Cén fáth ar mhaith leat a bheith páirteach?" ionchur téacs éigeantach seachas roghnach
@@ -131,17 +134,21 @@ ga:
         name: Ní féidir leat ach cásáil na litreacha a athrú, mar shampla, chun é a dhéanamh níos inléite
       terms_of_service:
         changelog: Is féidir é a struchtúrú le comhréir Markdown.
+        effective_date: Féadfaidh tréimhse ama réasúnta a bheith idir 10 agus 30 lá ón dáta a chuireann tú in iúl do d'úsáideoirí.
         text: Is féidir é a struchtúrú le comhréir Markdown.
       terms_of_service_generator:
         admin_email: Áirítear le fógraí dlí frithfhógraí, orduithe cúirte, iarratais éirí anuas, agus iarratais forghníomhaithe dlí.
-        arbitration_address: D’fhéadfadh sé a bheith mar an gcéanna leis an seoladh fisiceach thuas, nó “N/B” má tá ríomhphost in úsáid agat
-        arbitration_website: D’fhéadfadh gur foirm ghréasáin í, nó “N/A” má tá ríomhphost in úsáid agat
+        arbitration_address: D’fhéadfadh sé a bheith mar an gcéanna leis an seoladh fisiceach thuas, nó “N/B” má tá ríomhphost in úsáid agat.
+        arbitration_website: D’fhéadfadh gur foirm ghréasáin í, nó “N/B” má tá ríomhphost in úsáid agat.
+        choice_of_law: Cathair, réigiún, críoch nó stát a rialóidh a dhlíthe inmheánacha substaintiúla aon éileamh agus gach éileamh.
         dmca_address: Maidir le hoibreoirí SAM, bain úsáid as an seoladh atá cláraithe in Eolaire Gníomhairí Ainmnithe DMCA. A P.O. Tá liostú boscaí ar fáil ach é a iarraidh go díreach, bain úsáid as Iarratas Tarscaoilte Bosca Gníomhaire Ainmnithe DMCA chun ríomhphost a chur chuig an Oifig Cóipchirt agus déan cur síos gur modhnóir ábhar baile thú a bhfuil eagla ort díoltas nó aisíocaíocht a fháil mar gheall ar do ghníomhartha agus a dteastaíonn uait úsáid a bhaint as P.O. Bosca chun do sheoladh baile a bhaint den radharc poiblí.
-        dmca_email: Is féidir gurb é an ríomhphost céanna é a úsáidtear le haghaidh “Seoladh ríomhphoist le haghaidh fógraí dlí” thuas
+        dmca_email: Is féidir gurb é an ríomhphost céanna é a úsáidtear le haghaidh “Seoladh ríomhphoist le haghaidh fógraí dlí” thuas.
         domain: Aitheantas uathúil na seirbhíse ar líne atá á cur ar fáil agat.
         jurisdiction: Liostaigh an tír ina bhfuil cónaí ar an té a íocann na billí. Más cuideachta nó aonán eile é, liostaigh an tír ina bhfuil sé corpraithe, agus an chathair, an réigiún, an chríoch nó an stát mar is cuí.
+        min_age: Níor chóir go mbeidís faoi bhun na haoise íosta a éilíonn dlíthe do dhlínse.
       user:
         chosen_languages: Nuair a dhéantar iad a sheiceáil, ní thaispeánfar ach postálacha i dteangacha roghnaithe in amlínte poiblí
+        date_of_birth: Ní mór dúinn a chinntiú go bhfuil tú ar a laghad %{age} chun Mastodon a úsáid. Ní stórálfaimid é seo.
         role: Rialaíonn an ról na ceadanna atá ag an úsáideoir.
       user_role:
         color: Dath le húsáid don ról ar fud an Chomhéadain, mar RGB i bhformáid heicsidheachúlach
@@ -155,6 +162,7 @@ ga:
         url: An áit a seolfar imeachtaí chuig
     labels:
       account:
+        attribution_domains: Tá cead ag suíomhanna Gréasáin creidmheas a thabhairt duit
         discoverable: Próifíl gné agus postálacha in halgartaim fionnachtana
         fields:
           name: Lipéad
@@ -231,6 +239,7 @@ ga:
         setting_display_media_show_all: Taispeáin uile
         setting_expand_spoilers: Méadaigh postálacha atá marcáilte le rabhaidh inneachair i gcónaí
         setting_hide_network: Folaigh do ghraf sóisialta
+        setting_missing_alt_text_modal: Taispeáin dialóg deimhnithe sula bpostálann tú meán gan alt téacs
         setting_reduce_motion: Laghdú ar an tairiscint i beochan
         setting_system_font_ui: Úsáid cló réamhshocraithe an chórais
         setting_system_scrollbars_ui: Bain úsáid as scrollbharra réamhshocraithe an chórais
@@ -252,6 +261,7 @@ ga:
         name: Haischlib
       filters:
         actions:
+          blur: Folaigh na meáin le rabhadh
           hide: Cuir i bhfolach go hiomlán
           warn: Cuir i bhfolach le rabhadh
       form_admin_settings:
@@ -265,6 +275,7 @@ ga:
         favicon: Favicon
         mascot: Mascóg saincheaptha (oidhreacht)
         media_cache_retention_period: Tréimhse choinneála taisce meán
+        min_age: Riachtanas aoise íosta
         peers_api_enabled: Foilsigh liosta de na freastalaithe aimsithe san API
         profile_directory: Cumasaigh eolaire próifíle
         registrations_mode: Cé atá in ann clárú
@@ -330,16 +341,22 @@ ga:
         usable: Lig do phostálacha an hashchlib seo a úsáid go háitiúil
       terms_of_service:
         changelog: Cad atá athraithe?
+        effective_date: Dáta éifeachtach
         text: Téarmaí Seirbhíse
       terms_of_service_generator:
         admin_email: Seoladh ríomhphoist le haghaidh fógraí dlí
         arbitration_address: Seoladh fisiciúil le haghaidh fógraí eadrána
         arbitration_website: An láithreán gréasáin chun fógraí eadrána a chur isteach
+        choice_of_law: Rogha an Dlí
         dmca_address: Seoladh fisiciúil le haghaidh fógraí DMCA/cóipchirt
         dmca_email: Seoladh ríomhphoist le haghaidh fógraí DMCA/cóipchirt
         domain: Fearann
         jurisdiction: Dlínse dhlíthiúil
+        min_age: Aois íosta
       user:
+        date_of_birth_1i: Lá
+        date_of_birth_2i: Mí
+        date_of_birth_3i: Bliain
         role: Ról
         time_zone: Crios ama
       user_role:
diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml
index 1c3fa2278d..3773123f17 100644
--- a/config/locales/simple_form.gl.yml
+++ b/config/locales/simple_form.gl.yml
@@ -75,6 +75,7 @@ gl:
       filters:
         action: Elixe a acción a realizar cando algunha publicación coincida co filtro
         actions:
+          blur: Ocultar multimedia detrás dun aviso, sen ocultar o texto que se inclúa
           hide: Agochar todo o contido filtrado, facer coma se non existise
           warn: Agochar o contido filtrado tras un aviso que conteña o nome do filtro
       form_admin_settings:
@@ -88,6 +89,7 @@ gl:
         favicon: WEBP, PNG, GIF ou JPG. Sobrescribe a icona de favoritos de Mastodon por defecto cunha icona personalizada.
         mascot: Sobrescribe a ilustración na interface web avanzada.
         media_cache_retention_period: Os ficheiros multimedia de publicacións de usuarias remotas están almacenados no teu servidor. Ao establecer un valor positivo, o multimedia vaise eliminar despois do número de días establecido. Se o multimedia fose requerido após ser eliminado entón descargaríase outra vez, se aínda está dispoñible na orixe. Debido a restricións sobre a frecuencia en que o servizo de vista previa trae recursos de terceiras partes, é recomendable establecer este valor polo menos en 14 días, ou as tarxetas de vista previa non se actualizarán baixo demanda para casos anteriores a ese prazo.
+        min_age: Váiselle pedir ás usuarias que confirmen a súa data de nacemento cando creen a conta
         peers_api_enabled: Unha lista dos nomes de dominio que este servidor atopou no fediverso. Non se inclúen aquí datos acerca de se estás a federar con eles ou non, só que o teu servidor os recoñeceu. Ten utilidade para servizos que recollen estatísticas acerca da federación nun amplo senso.
         profile_directory: O directorio de perfís inclúe a tódalas usuarias que optaron por ser descubribles.
         require_invite_text: Cando os rexistros requiren aprobación manual, facer que o texto "Por que te queres rexistrar?" do convite sexa obrigatorio en lugar de optativo
@@ -132,17 +134,21 @@ gl:
         name: Só podes cambiar maiús/minúsculas, por exemplo, mellorar a lexibilidade
       terms_of_service:
         changelog: Pode usarse sintaxe Markdown.
+        effective_date: Un prazo razonable podería estar entre 10 e 30 días desde que envías a notificación ás usuarias.
         text: Pode usarse sintaxe Markdown.
       terms_of_service_generator:
         admin_email: Os avisos legais inclúen ordes xudiciais, solicitudes de retirada de contido e requerimentos do cumprimento da lei.
-        arbitration_address: Pode ser o mesmo enderezo Físico de máis arriba, ou "N/A" se usas o correo electrónico
-        arbitration_website: Pode ser un formulario web ou "N/A" se usas o correo electrónico
+        arbitration_address: Pode ser igual ao enderezo Físico de arriba, ou "N/A" se usas correo electrónico.
+        arbitration_website: Pode ser un formulario web, ou "N/A" se usas o correo electrónico.
+        choice_of_law: Cidade, rexión, territorio ou estado cuxas leis rexirán sobre as reclamacións e disputas.
         dmca_address: Para operadores de USA, utiliza o mesmo enderezo rexistrado no DMCA Designated Agent Directory. Está dispoñible a opción de usar P.O. Box facendo a solicitude baixo a DMCA Designated Agent Post Office Box Waiver Request pedíndoa á Copyright Office e indicando que es un moderador de contidos na túa casa e temes ameazas ou violencia polas medidas que tomas e precisas usar un P.O. Box para que non sexa público o enderezo da túa casa.
-        dmca_email: Pode ser o mesmo enderezo de correo usado para "Enderezo para avisos legais"
+        dmca_email: Pode ser o mesmo correo usado para "Enderezo de correo para avisos legais".
         domain: Identificador único do servizo en liña que proporcionas.
         jurisdiction: Informa sobre o país onde vive quen paga as facturas. Se é unha empresa ou outra entidade, indica o país onde está establecida e é axeitado escribir a cidade, rexión, territorio ou estado.
+        min_age: Non debería ser inferior á idade mínima requerida polas leis da túa xurisdición.
       user:
         chosen_languages: Se ten marca, só as publicacións nos idiomas seleccionados serán mostrados en cronoloxías públicas
+        date_of_birth: Temos que ter certeza de que tes %{age} como mínimo para usar Mastodon. Non gardamos este dato.
         role: Os roles establecen os permisos que ten a usuaria.
       user_role:
         color: Cor que se usará para o rol a través da IU, como RGB en formato hex
@@ -202,7 +208,7 @@ gl:
         current_password: Contrasinal actual
         data: Datos
         display_name: Nome mostrado
-        email: Enderezo de email
+        email: Enderezo de correo
         expires_in: Caduca tras
         fields: Metadatos do perfil
         header: Cabeceira
@@ -255,6 +261,7 @@ gl:
         name: Cancelo
       filters:
         actions:
+          blur: Ocultar multimedia cun aviso
           hide: Agochar completamente
           warn: Agochar tras un aviso
       form_admin_settings:
@@ -268,6 +275,7 @@ gl:
         favicon: Favicon
         mascot: Mascota propia (herdado)
         media_cache_retention_period: Período de retención da caché multimedia
+        min_age: Idade mínima requerida
         peers_api_enabled: Publicar na API unha lista dos servidores descubertos
         profile_directory: Activar o directorio de perfís
         registrations_mode: Quen se pode rexistrar
@@ -281,7 +289,7 @@ gl:
         site_terms: Política de Privacidade
         site_title: Nome do servidor
         status_page_url: URL da páxina do estado
-        theme: Decorado por defecto
+        theme: Decorado predeterminado
         thumbnail: Icona do servidor
         timeline_preview: Permitir acceso á cronoloxía pública sen autenticación
         trendable_by_default: Permitir tendencias sen aprobación previa
@@ -333,16 +341,22 @@ gl:
         usable: Permitir que as publicacións usen este cancelo localmente
       terms_of_service:
         changelog: Que cambios se fixeron?
+        effective_date: Data efectiva
         text: Termos do Servizo
       terms_of_service_generator:
         admin_email: Enderezo de correo para avisos legais
         arbitration_address: Enderezo físico para avisos de arbitraxe
         arbitration_website: Páxina web para enviar avisos de arbitraxe
+        choice_of_law: Xurisdición
         dmca_address: Enderezo físico para avisos sobre DMCA/copyright
         dmca_email: Enderezo de correo electrónico para avisos sobre DMCA/copyright
         domain: Dominio
         jurisdiction: Xurisdición legal
+        min_age: Idade mínima
       user:
+        date_of_birth_1i: Día
+        date_of_birth_2i: Mes
+        date_of_birth_3i: Ano
         role: Rol
         time_zone: Fuso horario
       user_role:
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index 6bf767adb3..9c01b91638 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -75,6 +75,7 @@ he:
       filters:
         action: בחרו איזו פעולה לבצע כאשר הודעה מתאימה למסנן
         actions:
+          blur: החבאת וידאו ותמונות מאחורי אזהרה, ללא החבאת המלל עצמו
           hide: הסתרת התוכן המסונן, כאילו לא היה קיים
           warn: הסתרת התוכן המסונן מאחורי אזהרה עם כותרת המסנן
       form_admin_settings:
@@ -88,6 +89,7 @@ he:
         favicon: WEBP, PNG, GIF או JPG. גובר על "פאבאייקון" ברירת המחדל ומחליף אותו באייקון נבחר בדפדפן.
         mascot: בחירת ציור למנשק הווב המתקדם.
         media_cache_retention_period: קבצי מדיה מהודעות שהגיעו משרתים רחוקים נשמרות על השרת שלך. כאשר יבחר פה מספר חיובי, המדיה תמחק לאחר מספר ימים כמצוין. אם המידע יבוקש שוב לאחר שנמחק, הוא יורד מחדש, אם המידע עדיין זמין בצד הרחוק. עקב מגבלות על תכיפות שליפת כרטיסי קדימון מאתרים מרוחקים, מומלץ לכוון את הערך ל־14 יום לפחות, או שכרטיסי קדימונים לא יעודכנו לפי דרישה לפני חלוף חלון הזמן הזה.
+        min_age: משתמשיםות יתבקשו לאשר את תאריך הלידה בתהליך ההרשמה
         peers_api_enabled: רשימת השרתים ששרת זה פגש בפדיוורס. לא כולל מידע לגבי קשר ישיר עם שרת נתון, אלא רק שידוע לשרת זה על קיומו. מידע זה משמש שירותים האוספים סטטיסטיקות כלליות על הפדרציה.
         profile_directory: ספריית הפרופילים מציגה ברשימה את כל המשתמשים שביקשו להיות ניתנים לגילוי.
         require_invite_text: כאשר הרשמות דורשות אישור ידני, הפיכת טקסט ה"מדוע את/ה רוצה להצטרף" להכרחי במקום אופציונלי
@@ -132,17 +134,21 @@ he:
         name: ניתן רק להחליף בין אותיות קטנות וגדולות, למשל כדי לשפר את הקריאות
       terms_of_service:
         changelog: ניתן לעצב בעזרת תחביר מארקדאון.
+        effective_date: לוח זמנים סביר יהיה בין 10-30 ימים מיום ההודעה למשתמשיםות שלך.
         text: ניתן לעצב בעזרת תחביר מארקדאון.
       terms_of_service_generator:
         admin_email: מזכרים חוקיים כוללים הוראות בימ"ש, בקשות הסרה, תשובות להתראות ובקשות ציות לאכיפה.
-        arbitration_address: יכול להיות כתובת פיזית זהה לעיל, או “N/A” אם משתמשים בדואל
-        arbitration_website: יכול להיות טופס בדפדפן או “N/A” אם משתמשים בדואל
+        arbitration_address: יכול להיות כתובת פיזית זהה לעיל, או “N/A” אם משתמשים בדואל.
+        arbitration_website: יכול להיות טופס בדפדפן או “N/A” אם משתמשים בדואל.
+        choice_of_law: עיר, מחוז, טריטוריה או מדינה שחוקיה חלים על כל התביעות והתלונות.
         dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
-        dmca_email: ניתן להשתמש באותה כתובת הדואל שעבור "כתובת דואל להודעות משפטיות" לעיל
+        dmca_email: ניתן להשתמש באותה כתובת הדואל שעבור "כתובת דואל להודעות משפטיות" לעיל.
         domain: זיהוי ייחודי של השירות המקוון שאתה מספק.
         jurisdiction: פרט את המדינה שבה גר משלם החשבונות. אם מדובר בחברה או גוף אחר, פרטו את המדינה שבה הוקם התאגיד, העיר, האזור, או המדינה בהתאם לצורך.
+        min_age: על הערך להיות לפחות בגיל המינימלי הדרוש בחוק באיזור השיפוט שלך.
       user:
         chosen_languages: אם פעיל, רק הודעות בשפות הנבחרות יוצגו לפידים הפומביים
+        date_of_birth: עלינו לוודא שגילך לפחות %{age} כדי להשתמש במסטודון. המידע לא ישמר בשרת שלנו.
         role: התפקיד שולט על אילו הרשאות יש למשתמש.
       user_role:
         color: צבע לתפקיד בממשק המשתמש, כ RGB בפורמט הקסדצימלי
@@ -255,6 +261,7 @@ he:
         name: תגית
       filters:
         actions:
+          blur: הסתרת מדיה עם אזהרה
           hide: הסתרה כוללת
           warn: הסתרה עם אזהרה
       form_admin_settings:
@@ -268,6 +275,7 @@ he:
         favicon: סמל מועדפים (Favicon)
         mascot: סמל השרת (ישן)
         media_cache_retention_period: תקופת שמירת מטמון מדיה
+        min_age: דרישת גיל מינימלי
         peers_api_enabled: פרסם רשימה של שרתים שנתגלו באמצעות ה-API
         profile_directory: הפעלת ספריית פרופילים
         registrations_mode: מי יכולים לפתוח חשבון
@@ -333,16 +341,22 @@ he:
         usable: הרשה/י להודעות להכיל תגית זו באופן מקומי
       terms_of_service:
         changelog: מה נשתנה?
+        effective_date: תאריך תחולה
         text: תנאי השירות
       terms_of_service_generator:
         admin_email: כתובת דוא"ל להודעות משפטיות
         arbitration_address: כתובת פיזית להודעות גישור
         arbitration_website: אתר רשת להגשת הודעות גישור
+        choice_of_law: בחירת אזור שיפוט
         dmca_address: כתובת פיזית לשליחת התראות זכויות יוצרים/DMCA
         dmca_email: כתובת דואל לשליחת התראות זכויות יוצרים/DMCA
         domain: מתחם (דומיין)
         jurisdiction: איזור השיפוט
+        min_age: גיל מינימלי
       user:
+        date_of_birth_1i: יום
+        date_of_birth_2i: חודש
+        date_of_birth_3i: שנה
         role: תפקיד
         time_zone: אזור זמן
       user_role:
diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml
index 1b236b9356..adcc3d5789 100644
--- a/config/locales/simple_form.hu.yml
+++ b/config/locales/simple_form.hu.yml
@@ -75,6 +75,7 @@ hu:
       filters:
         action: A végrehajtandó műveletet, ha a bejegyzés megfelel a szűrőnek
         actions:
+          blur: Média elrejtése figyelmeztetéssel, a szöveg elrejtése nélkül
           hide: A szűrt tartalom teljes elrejtése, mintha nem is létezne
           warn: A szűrt tartalom a szűrő címét említő figyelmeztetés mögé rejtése
       form_admin_settings:
@@ -88,6 +89,7 @@ hu:
         favicon: WEBP, PNG, GIF vagy JPG. Az alapértelmezett Mastodon favicont felülírja egy egyéni ikonnal.
         mascot: Felülbírálja a speciális webes felületen található illusztrációt.
         media_cache_retention_period: A távoli felhasználók bejegyzéseinek médiatartalmait a kiszolgálód gyorsítótárazza. Ha pozitív értékre állítják, ezek a médiatartalmak a megadott számú nap után törölve lesznek. Ha a médiát újra lekérik, miután törlődött, újra le fogjuk tölteni, ha az eredeti még elérhető. A hivatkozások előnézeti kártyáinak harmadik fél weboldalai felé történő hivatkozásaira alkalmazott megkötései miatt javasolt, hogy ezt az értéket legalább 14 napra állítsuk, ellenkező esetben a hivatkozások előnézeti kártyái szükség esetén nem fognak tudni frissülni ezen idő előtt.
+        min_age: A felhasználók a regisztráció során arra lesznek kérve, hogy erősítsek meg a születési dátumukat
         peers_api_enabled: Azon domainek listája, melyekkel ez a kiszolgáló találkozott a fediverzumban. Nem csatolunk adatot arról, hogy föderált kapcsolatban vagy-e az adott kiszolgálóval, csak arról, hogy a kiszolgálód tud a másikról. Ezt olyan szolgáltatások használják, melyek általában a föderációról készítenek statisztikákat.
         profile_directory: A profilok jegyzéke minden olyan felhasználót felsorol, akik engedélyezték a felfedezhetőségüket.
         require_invite_text: Ha a regisztrációhoz manuális jóváhagyásra van szükség, akkor a „Miért akarsz csatlakozni?” válasz kitöltése legyen kötelező, és ne opcionális
@@ -132,14 +134,21 @@ hu:
         name: Csak a kis/nagybetűséget változtathatod meg, pl. hogy olvashatóbb legyen
       terms_of_service:
         changelog: Markdown szintaxissal adható meg.
+        effective_date: Egy elfogadható időablak a felhasználók értesítésétől számított 10 és 30 nap között bármi lehet.
         text: Markdown szintaxissal adható meg.
       terms_of_service_generator:
         admin_email: A jogi közlemények közé tartoznak az ellenkeresetek, a bírósági végzések, az eltávolítási kérelmek és a bűnüldöző szervek kérelmei is.
-        arbitration_address: Lehet a fent használt valós cím, vagy e-mail használata esetén „N/A”
-        arbitration_website: Lehet webes űrlap, vagy e-mail használata esetén „N/A”
+        arbitration_address: Lehet a fent használt valós cím, vagy e-mail használata esetén „N/A”.
+        arbitration_website: Lehet webes űrlap, vagy e-mail használata esetén „N/A”.
+        choice_of_law: Város, régió, terület vagy állam, amely jogszabályai irányadóak az összes követelés esetén.
+        dmca_address: Amerikai üzemeltető esetén használja a DMCA Designated Agent Directory jegyzékében regisztrált címet. Közvetlen kérésre postafiók-jegyzék is rendelkezésre áll, használja a DMCA Designated Agent Directory postafiókról való lemondás iránti kérelmét a Copyright Office-nak küldött e-mailben, és írja le, hogy Ön egy otthoni tartalommoderátor, aki bosszútól vagy megtorlástól tart a tevékenységi miatt, és ezért egy postafiókra van szüksége, hogy ne legyen nyilvános az otthoni címe.
+        dmca_email: Megegyezhet a fenti „E-mail-cím a jogi nyilatkozatok benyújtásához” mezővel.
         domain: A nyújtott online szolgáltatás egyedi azonosítója.
+        jurisdiction: Adja meg az országot, ahol a számlafizető él. Ha ez egy vállalat vagy más szervezet, adja meg az országot, ahol bejegyezték, valamint adott esetben a várost, régiót, területet vagy államot.
+        min_age: Nem lehet a joghatóság által meghatározott minimális kor alatt.
       user:
         chosen_languages: Ha aktív, csak a kiválasztott nyelvű bejegyzések jelennek majd meg a nyilvános idővonalon
+        date_of_birth: Legalább %{age} évesnek kell lenniük, hogy használhassák a Mastodont. Ezt nem tároljuk.
         role: A szerep szabályozza, hogy a felhasználó milyen jogosultságokkal rendelkezik.
       user_role:
         color: A szerephez használandó szín mindenhol a felhasználói felületen, hexa RGB formátumban
@@ -252,6 +261,7 @@ hu:
         name: Hashtag
       filters:
         actions:
+          blur: Média elrejtése figyelmeztetéssel
           hide: Teljes elrejtés
           warn: Elrejtés figyelmeztetéssel
       form_admin_settings:
@@ -265,6 +275,7 @@ hu:
         favicon: Könyvjelzőikon
         mascot: Egyéni kabala (örökölt)
         media_cache_retention_period: Média-gyorsítótár megtartási időszaka
+        min_age: Minimális korhatár
         peers_api_enabled: Felfedezett kiszolgálók listájának közzététele az API-ban
         profile_directory: Profiladatbázis engedélyezése
         registrations_mode: Ki regisztrálhat
@@ -330,16 +341,22 @@ hu:
         usable: A helyi bejegyzések használhatják ezt a hashtaget
       terms_of_service:
         changelog: Mi változott?
+        effective_date: A hatálybalépés dátuma
         text: Felhasználási feltételek
       terms_of_service_generator:
         admin_email: E-mail-cím a jogi nyilatkozatok benyújtásához
         arbitration_address: Fizikai cím a békéltető testületi nyilatkozatok benyújtásához
         arbitration_website: Weboldal a békéltető testületi nyilatkozatok benyújtásához
+        choice_of_law: Joghatóság
         dmca_address: Fizikai cím a szerzői jogi nyilatkozatokhoz
         dmca_email: E-mail-cím a szerzői jogi nyilatkozatokhoz
         domain: Domain
         jurisdiction: Joghatóság
+        min_age: Minimális életkor
       user:
+        date_of_birth_1i: Nap
+        date_of_birth_2i: Hónap
+        date_of_birth_3i: Év
         role: Szerep
         time_zone: Időzóna
       user_role:
diff --git a/config/locales/simple_form.ia.yml b/config/locales/simple_form.ia.yml
index 5ede26f9ad..cb9b2a3da5 100644
--- a/config/locales/simple_form.ia.yml
+++ b/config/locales/simple_form.ia.yml
@@ -58,8 +58,8 @@ ia:
         setting_always_send_emails: Normalmente, le notificationes de e-mail non es inviate quando tu activemente usa Mastodon
         setting_default_sensitive: Le medios sensibile es celate de ordinario e pote esser revelate con un clic
         setting_display_media_default: Celar le medios marcate como sensibile
-        setting_display_media_hide_all: Sempre celar le medios
-        setting_display_media_show_all: Sempre monstrar le medios
+        setting_display_media_hide_all: Sempre celar contento multimedial
+        setting_display_media_show_all: Sempre monstrar contento multimedial
         setting_system_scrollbars_ui: Se applica solmente al navigatores de scriptorio basate sur Safari e Chrome
         setting_use_blurhash: Le imagines degradate se basa sur le colores del visuales celate, ma illos offusca tote le detalios
         setting_use_pending_items: Requirer un clic pro monstrar nove messages in vice de rolar automaticamente le fluxo
@@ -135,10 +135,8 @@ ia:
         text: Pote esser structurate con le syntaxe Markdown.
       terms_of_service_generator:
         admin_email: Le avisos juridic include le contra-avisos, ordinantias judiciari, demandas de retiro e demandas de application del lege.
-        arbitration_address: Pote esser le mesme que le adresse physic supra, o “N/A” si se usa e-mail
-        arbitration_website: Pote esser un formulario web, o “N/A” si se usa e-mail
+        choice_of_law: Citate, region, territorio o stato cuje leges substantive interne governa omne disputa juridic.
         dmca_address: Pro operatores in le SUA, usa le adresse registrate in le Directorio de Agentes Designate pro le DMCA (DMCA Designated Agent Directory). Un adresse de cassa postal es disponibile per requesta directe; usa le Requesta de Exemption de Cassa Postal pro Agentes Designate del DMCA (DMCA Designated Agent Post Office Box Waiver Request) pro inviar un message electronic al Officio del Derecto de Autor (Copyright Office) e describer que tu es un moderator de contento que travalia de casa e qui time vengiantias o represalias pro tu actiones, necessitante le uso un cassa postal pro remover tu adresse personal del vista public.
-        dmca_email: Pote esser le mesme adresse de e-mail usate pro “Adresse de e-mail pro avisos juridic” supra
         domain: Identification unic del servicio in linea que tu forni.
         jurisdiction: Indica le pais ubi vive le persona qui paga le facturas. Si se tracta de un interprisa o altere organisation, indica le pais ubi illo es incorporate, e le citate, region, territorio o stato del maniera appropriate pro le pais.
       user:
@@ -233,6 +231,7 @@ ia:
         setting_display_media_show_all: Monstrar toto
         setting_expand_spoilers: Sempre expander messages marcate con avisos de contento
         setting_hide_network: Cela tu rete social
+        setting_missing_alt_text_modal: Monstrar un dialogo de confirmation ante de publicar multimedia sin texto alternative
         setting_reduce_motion: Reducer movimento in animationes
         setting_system_font_ui: Usar typo de litteras predefinite del systema
         setting_system_scrollbars_ui: Usar le barra de rolamento predefinite del systema
@@ -337,6 +336,7 @@ ia:
         admin_email: Adresse de e-mail pro avisos juridic
         arbitration_address: Adresse physic pro avisos de arbitration
         arbitration_website: Sito web pro submitter avisos de arbitration
+        choice_of_law: Election del lege applicabile
         dmca_address: Adresse physic pro avisos DCMA/de derectos de autor
         dmca_email: Adresse de e-mail pro avisos DCMA/de derectos de autor
         domain: Dominio
diff --git a/config/locales/simple_form.io.yml b/config/locales/simple_form.io.yml
index 631e7182fd..7da9441ba6 100644
--- a/config/locales/simple_form.io.yml
+++ b/config/locales/simple_form.io.yml
@@ -3,12 +3,14 @@ io:
   simple_form:
     hints:
       account:
+        attribution_domains: Un en singla lineo.
         discoverable: Vua publika posti e profilo povas remarkesar o rekomendesar en diferanta parti di Mastodon e vua profilo povas sugestesar ad altra uzanti.
         display_name: Vua tota nomo o vua gaya nomo.
         fields: Vua retsituo, pronomi, evo, irgo quan vu volas.
         indexable: Vua posta publika povos aparar en rezultaji di serchi che Mastodon. Personi qui interagis kun vua posti povos serchar oli irgakaze.
         note: 'Vu povas @mencionar altra personi o #hashtagi.'
         show_collections: Personi povos navigar tra vua sequati e sequanti. Personi quin vu sequas, vidos ke vu sequas li irgakaze.
+        unlocked: Personi povos sequar vu sen demandar aprobo.
       account_alias:
         acct: Partikulare pozez uzantonomo@domeno di konto quon vua volas ektransferesar
       account_migration:
@@ -19,15 +21,15 @@ io:
       admin_account_action:
         include_statuses: Uzanto vidos quala posti quo kauzigas jero o averto
         send_email_notification: Uzanto ganos expliko pri quo eventas a olia konto
-        text_html: Neobligata. Vu povas uzar postosintaxo. Vu povas <a href="%{path}">insertar avertofixiti</a> por sparar tempo
+        text_html: Nemustiga. Vu povas <a href="%{path}">adjuntar avertselektaji</a> por sparar tempo
         type_html: Selektez ago per <strong>%{acct}</strong>
         types:
           disable: Preventez uzanto de uzar olia konto ma ne efacez o celez olia kontenaji.
           none: Uzez co por sendar averto a la uzanto sen eventigar irga altra ago.
-          sensitive: Koaktez omna mediiatachaji da ca uzanto markizesar quale sentoza.
+          sensitive: Igar omna audvidajaddonaji da ca uzanto markesar quale trublema.
           silence: Preventez la uzanto de povar postar per publika videbleso, celez lua posti e avizi de personi qui ne sequas lu. Co klozas omna raporto kontra ca konto.
           suspend: Preventez irga interago de o a ca konto e efacez lua kontenaji. Inversebla til 30 dii. Co klozas omna raporti kontra ca konto.
-        warning_preset_id: Neobligata. Vu povas ankore insertar kustume texto a extremajo di fixito
+        warning_preset_id: Nemustiga
       announcement:
         all_day: Kande kontrolesas, nur tempoporteodato montresos
         ends_at: Neobligata. Anunco automatika depublikigesos dum ta tempo
@@ -54,12 +56,13 @@ io:
         scopes: Quala API quon softwaro permisesas acesar. Se vu selektas alta skopo, vu ne mustas selektar individui.
         setting_aggregate_reblogs: Ne montrez nova repeti di posti qui ja repetesis recente (nur efektigas repeti recevata nove)
         setting_always_send_emails: Normale retpostoavizi ne sendesas kande vu aktiva uzas Mastodon
-        setting_default_sensitive: Sentoza medii originala celesas e povas revelesar per klikto
-        setting_display_media_default: Celez medii quo markizesis quale sentoza
-        setting_display_media_hide_all: Sempre celez medii
-        setting_display_media_show_all: Sempre montrez medii
+        setting_default_sensitive: Trublema audvidaji originala celesas e povas descelesar per kliko
+        setting_display_media_default: Celez audvidaji qua markesis quale trublema
+        setting_display_media_hide_all: Omnatempe celas audvidaji
+        setting_display_media_show_all: Omnatempe montras audvidaji
+        setting_system_scrollbars_ui: Nur ye tablokomputilretumili qua bazita ye Safario e Kromeo
         setting_use_blurhash: Inklini esas segun kolori di celesis vidaji ma kovras irga detali
-        setting_use_pending_items: Celez tempolineonovi dop kliktar e ne automatike movigar niuzeto
+        setting_use_pending_items: Celez tempolinetildatigo dop kliko vice automatike ruligar la fluo
         username: Vu darfas uzar literi, nombri, e sublinei
         whole_word: Kande klefvorto o fraz esas nur litera e nombra, ol nur aplikesos se ol parigesas la tota vorto
       domain_allow:
@@ -77,17 +80,17 @@ io:
       form_admin_settings:
         activity_api_enabled: Quanto de lokale publikigita posti, aktiva uzanti e nova registri, donita semanope
         app_icon: WEBP, PNG, GIF o JPG. Ol remplas la originala imajeto di softwaro sur poshaparati kun personaligita imajeto.
-        backups_retention_period: Uzanto povas facar arkivi di sua posti por deskargar pose. Se ol esas positiva nombro, ca arkivi automate efacesis de vua konserveyo pos la decidita quanto di dii.
-        bootstrap_timeline_accounts: Ca konti pinglagesos a super sequorekomendi di nova uzanti.
+        backups_retention_period: Uzanto povas igar arkivi di sua afishi por deskargar pose.
+        bootstrap_timeline_accounts: Ca konti adpinglesos ad super sequorekomendi di nova uzanti.
         closed_registrations_message: Montresas kande registradi klozesas
         content_cache_retention_period: Omna posti de altra servili efacesos (anke repeti e respondi) pos decidita quanto di dii, sen ye irga lokala uzantointerago kun ti posti. Privata mencioni inter uzanto de dessanta servili anke desganos e neposible riganesos.
         custom_css: Vu povas pozar kustumizita staili en retverso di Mastodon.
         favicon: WEBP, PNG, GIF o JPG. Ol remplas la originala imajeto di Mastodon kun personaligita imajeto.
         mascot: Remplas montreso en avanca retintervizajo.
-        media_cache_retention_period: Medidoseri de posti quan posti da deslokala uzanti retummemoresis sur vua servilo. Se medidatumo demandesas pos ol efacesas, ol rideskargesos.
+        media_cache_retention_period: Audvidajdosieri di afishi da deslokala uzanti retmemoresis sur vua servilo. Se audvidajdatumo demandesas pos ol forigesis, ol rideskargesos.
         peers_api_enabled: Listo di domeni quin ca servilo trovis en la fediverso. Nula informo inkluzesas hike pri ka vu federas kun partikulara servilo, nur ke vua servilo savas pri lo. Co es uzata da enti qui kolektas statistiki pri federeso generale.
-        profile_directory: La profilcheflisto montras omna uzanti quo voluntale volas esar deskovrebla.
-        require_invite_text: Kande registradi bezonas manuala aprobo, ol kauzigas "Por quo vu volas juntas?" textoenpozo esar obliganta
+        profile_directory: La profiluyo listigas omna uzanti qua volunte esar deskovrebla.
+        require_invite_text: Kande registragi bezonas manuala aprobo, "Por quo vu volas adeskar?" textoenpoza esos mustiga
         site_contact_email: Quale personi povas kontaktar vu por legala o suportquestioni.
         site_contact_username: Quale personi povas kontaktar vu en Mastodon.
         site_extended_description: Irga plusa informi quo forsan esar utila por vizitanti e uzanti. Povas strukturigesar per sintaxo di Markdown.
@@ -98,8 +101,8 @@ io:
         theme: Temo quo videsas da ekirita vizitanti e nova uzanti.
         thumbnail: Cirkum 2:1 imajo montresar kun informo di ca servilo.
         timeline_preview: Ekirita vizitanti videsos maxim recenta publika posti quo esas displonebla en la servilo.
-        trendable_by_default: Ignorez manuala kontrolar di tendencoza kontenajo. Singla kozi povas ankore efacesar de tendenci pose.
-        trends: Tendenci montras quala posti, hashtagi e niuzrakonti famozeskas en ca servilo.
+        trendable_by_default: Ignorez manuala kontrolar di populara enhavajo.
+        trends: Populari montras quala afishi, gretvorti e novaji populareskas en vua servilo.
         trends_as_landing_page: Montrez populara posti a uzanti neeniriti e vizitanti vice deskriptajo pri ca servilo. Bezonas ke populari es aktivita.
       form_challenge:
         current_password: Vu eniras sekura areo
@@ -108,7 +111,7 @@ io:
       invite_request:
         text: Co helpos ni kontrolar vua apliko
       ip_block:
-        comment: Neobligata. Memorez por quo vu insertas ca regulo.
+        comment: Nemustiga.
         expires_in: IP-Adresi esas finita moyeno, oli kelkafoye partigesas e frequenta uzesas da multa personi. Do, nedefinita IP-restrikti ne rekomendesas.
         ip: Tipez adreso di IPv4 o IPv6. Vu povas restrikar tota porteo per sintaxo CIDR. Sorgemez por ke vu ne klefklozas su!
         severities:
@@ -121,12 +124,21 @@ io:
         text: Deskriptez regulo o bezonaj por uzanti en ca servilo. Vu debas lasar lu esar korta e simpla
       sessions:
         otp: Enter the Two-factor code from your phone or use one of your recovery codes.
-        webauthn: Se ol esas klefo di USB, certigar ke vu insertas e se bezonesas, tushetez.
+        webauthn: Se ol es USB-klefo, certigar ke vu enpozas e se bezonesas, tushetez ol.
       settings:
         indexable: Vua profilpagino povas aparar en serchorezultaji che Google, Bing, e altri.
         show_application: Vu sempre povos vidar qua apliko publikigis vua posto irgakaze.
       tag:
         name: Vu povas nur chanjar literkaso, por exemplo, por kauzigar lu divenar plu lektebla
+      terms_of_service:
+        changelog: Povas strukturesita kun Markdown-sintaxo.
+        text: Povas strukturesita kun Markdown-sintaxo.
+      terms_of_service_generator:
+        admin_email: Legala notici inkluzas kontranotici, judicikomandi, forigdemandi e legaddemandi.
+        choice_of_law: Urbo, regiono, teritorio o stato qua havas ca interna inkluziva legi.
+        dmca_address: Por jeranti ek Usa, uzez la adreso en DMCA e demandez forigo di vua hemadreso.
+        domain: Unika identifikeso di enreta servo qua vu provizas.
+        jurisdiction: Listigez lando di paganto di fakturi.
       user:
         chosen_languages: Kande marketigesis, nur posti en selektesis lingui montresos en publika tempolinei
         role: La rolo donas certena permisi a la uzanto.
@@ -142,6 +154,7 @@ io:
         url: Ibe eventi sendesos
     labels:
       account:
+        attribution_domains: Reteyi kua permisesis agnoskar vu
         discoverable: Inkluzar profilo e posti en trovado-algoritmi
         fields:
           name: Etiketo
@@ -190,7 +203,7 @@ io:
         email: Retpost-adreso
         expires_in: Expiras pos
         fields: Profilmetadato
-        header: Kapimajo
+        header: Fundimajo
         honeypot: "%{label} (ne plenigez)"
         inbox_url: URL di relayomesajbuxo
         irreversible: Deslevez e ne celez
@@ -201,29 +214,31 @@ io:
         otp_attempt: Dufaktora identigilo
         password: Pasvorto
         phrase: Klefvorto o frazo
-        setting_advanced_layout: Aktivigez avancata retintervizajo
+        setting_advanced_layout: Ebligar altnivela retintervizajo
         setting_aggregate_reblogs: Grupigar repeti en tempolinei
         setting_always_send_emails: Sempre sendez retpostoavizi
-        setting_auto_play_gif: Automate pleez animigita GIFi
+        setting_auto_play_gif: Autoplear anima GIFi
         setting_boost_modal: Montrez konfirmdialogo ante repetar
         setting_default_language: Postolinguo
         setting_default_privacy: Videbleso di la mesaji
-        setting_default_sensitive: Sempre markizez medii quale sentoza
+        setting_default_sensitive: Omnatempe markas audvidaji quale trublema
         setting_delete_modal: Montrez konfirmdialogo ante efacar posto
-        setting_disable_hover_cards: Desaktivigez profilprevido dum klikpaso
-        setting_disable_swiping: Desaktivigez fingromovi
-        setting_display_media: Mediomontrajo
+        setting_disable_hover_cards: Desebligar profilprevido dum paso
+        setting_disable_swiping: Desebligar fingromovi
+        setting_display_media: Audvidajmontrajo
         setting_display_media_default: Originalo
         setting_display_media_hide_all: Celez omno
         setting_display_media_show_all: Montrez omno
         setting_expand_spoilers: Sempre expansigez posti quo markizesis kun kontenajaverti
         setting_hide_network: Celez vua sociala grafiko
-        setting_reduce_motion: Diminutez moveso di animacii
+        setting_missing_alt_text_modal: Montrar konfirmdialogo ante afishar audvidaji sen alternative texto
+        setting_reduce_motion: Despluigar movo di animi
         setting_system_font_ui: Uzez originala literfonto di sistemo
-        setting_theme: Sittemo
-        setting_trends: Montrez tendenco di hodie
+        setting_system_scrollbars_ui: Uzar originala rullangeto di sistemo
+        setting_theme: Reteytemo
+        setting_trends: Montrar hodia populari
         setting_unfollow_modal: Montrez konfirmdialogo ante desequar ulu
-        setting_use_blurhash: Montrez koloroza inklini por celata medii
+        setting_use_blurhash: Montrez koloroza inklini por celata audvidaji
         setting_use_pending_items: Modo lenta
         severity: Severeso
         sign_in_token_attempt: Sekureskodexo
@@ -250,11 +265,11 @@ io:
         custom_css: Kustumizita CSS
         favicon: Imajeto
         mascot: Kustumizita reprezentimajo (oldo)
-        media_cache_retention_period: Mediimemorajretendurtempo
+        media_cache_retention_period: Audvidajretmemorretendurtempo
         peers_api_enabled: Publikigez listo di deskovrita servili en API
-        profile_directory: Aktivigez profilcheflisto
+        profile_directory: Ebligar profiluyo
         registrations_mode: Qua povas registragar
-        require_invite_text: Mustez pozar motivo por juntar
+        require_invite_text: Bezonas motivo por adeskar
         show_domain_blocks: Montrez domenobstrukti
         show_domain_blocks_rationale: Montrez por quo domeni obstruktesir
         site_contact_email: Kontaktoretposto
@@ -267,8 +282,8 @@ io:
         theme: Originala temo
         thumbnail: Servilimajeto
         timeline_preview: Permisez neyurizita aceso a publika tempolineo
-        trendable_by_default: Permisez tendenci sen bezonar kontrolo
-        trends: Aktivigez tendenci
+        trendable_by_default: Permisez populari sen kontrolo
+        trends: Ebligar populari
         trends_as_landing_page: Uzar populari quale la iniciala pagino
       interactions:
         must_be_follower: Celar la savigi da homi, qui ne sequas tu
@@ -277,7 +292,7 @@ io:
       invite:
         comment: Komento
       invite_request:
-        text: Por quo vu volas juntar?
+        text: Por quo vu volas adeskar?
       ip_block:
         comment: Komento
         ip: IP
@@ -302,7 +317,7 @@ io:
           label: Nova Mastodon-versiono es disponebla
           none: Nultempe notifikar pri aktualigi (ne rekomendata)
           patch: Notifikar pri problemosolvanta aktualigi
-        trending_tag: Nova tendenco bezonas kontrolo
+        trending_tag: Nova popularo bezonas kontrolo
       rule:
         hint: Plusa informo
         text: Regulo
@@ -312,8 +327,20 @@ io:
       tag:
         listable: Permisez ca hashtago aparar en trovaji e sugestaji
         name: Hashtago
-        trendable: Permisez ca hashtago aparar che tendenci
+        trendable: Permisez ca gretvorto aparar en populari
         usable: Permisez posti uzar ca gretiketo lokale
+      terms_of_service:
+        changelog: Quo chanjesis?
+        text: Servtermini
+      terms_of_service_generator:
+        admin_email: Retpostoadreso por legala notici
+        arbitration_address: Fizika adreso por judicinotici
+        arbitration_website: Reteyo por sendar judicinotici
+        choice_of_law: Legselekteso
+        dmca_address: Fizika adreso por autoroyurnotici
+        dmca_email: Retpostoadreso por autoroyurnotici
+        domain: Domeno
+        jurisdiction: Legala seciono
       user:
         role: Rolo
         time_zone: Klokozono
@@ -324,7 +351,7 @@ io:
         permissions_as_keys: Permisi
         position: Prioreso
       webhook:
-        events: Aktivigita eventi
+        events: Ebligita eventi
         template: Pagkargshablono
         url: URL di finpunto
     'no': Ne
diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml
index ad755e5bb9..bc68d14f2a 100644
--- a/config/locales/simple_form.is.yml
+++ b/config/locales/simple_form.is.yml
@@ -75,6 +75,7 @@ is:
       filters:
         action: Veldu hvaða aðgerð á að framkvæma þegar færsla samsvarar síunni
         actions:
+          blur: Fela myndefni á bakvið aðvörun, án þess að fela sjálfann textann
           hide: Fela síað efni algerlega, rétt eins og það sé ekki til staðar
           warn: Fela síað efni á bakvið aðvörun sem tekur fram titil síunnar
       form_admin_settings:
@@ -88,6 +89,7 @@ is:
         favicon: WEBP, PNG, GIF eða JPG. Tekur yfir sjálfgefna Mastodon favicon-táknmynd með sérsniðinni táknmynd.
         mascot: Þetta tekyr yfir myndskreytinguna í ítarlega vefviðmótinu.
         media_cache_retention_period: Myndefnisskrár úr færslum sem gerðar eru af fjartengdum notendum eru geymdar á netþjóninum þínum. Þegar þetta er stillt á jákvætt gildi, verður þessum skrám eytt sjáfkrafa eftir þeim tiltekna fjölda daga. Ef beðið er um myndefnið eftir að því er eytt, mun það verða sótt aftur ef frumgögnin eru ennþá aðgengileg. Vegna takmarkana á hversu oft forskoðunarspjöld tengla eru sótt á utanaðkomandi netþjóna, þá er mælt með því að setja þetta gildi á að minnsta kosti 14 daga, annars gæti mistekist að uppfæra forskoðunarspjöld tengla eftir þörfum fyrir þann tíma.
+        min_age: Notendur verða beðnir um að staðfesta fæðingardag sinn við nýskráningu
         peers_api_enabled: Listi yfir þau lénaheiti sem þessi netþjónn hefur rekist á í skýjasambandinu. Engin gögn eru hér sem gefa til kynna hvort þú sért í sambandi við tiltekinn netþjón, bara að netþjónninn þinn viti um hann. Þetta er notað af þjónustum sem safna tölfræði um skýjasambönd á almennan hátt.
         profile_directory: Notendamappan telur upp alla þá notendur sem hafa valið að vera uppgötvanlegir.
         require_invite_text: Þegar nýskráningar krefjast handvirks samþykkis, þá skal gera textann í “Hvers vegna viltu taka þátt?” að kröfu en ekki valkvæðan
@@ -132,17 +134,21 @@ is:
         name: Þú getur aðeins breytt stafstöði mill há-/lágstafa, til gæmis til að gera þetta læsilegra
       terms_of_service:
         changelog: Er hægt að sníða með Markdown-málskipan.
+        effective_date: Ásættanlegur tímarammi getur verið frá 10 til 30 dagar eftir að þú hefur látið notendurna þína vita.
         text: Er hægt að sníða með Markdown-málskipan.
       terms_of_service_generator:
         admin_email: Löglegar tilkynningar ná yfir andsvör, dómsúrskurði, lokunarbeiðnir og beiðnir frá lögregluembættum.
-        arbitration_address: Má vera það sama og raunverulegt heimilisfang eða “N/A” ef tölvupóstur er notaður
-        arbitration_website: Má vera innfyllingarform á vefsíðu eða “N/A” ef tölvupóstur er notaður
+        arbitration_address: Má vera það sama og raunverulegt heimilisfang eða “N/A” ef tölvupóstur er notaður.
+        arbitration_website: Má vera innfyllingarform á vefsíðu eða “N/A” ef tölvupóstur er notaður.
+        choice_of_law: Sveitarfélög, héruð eða ríki þar sem ríkjandi lög og reglugerðir skulu stýra meðhöndlun á öllum kröfum.
         dmca_address: Fyrir rekstraraðila í BNA ætti að nota heimilisfang sem skráð er í DMCA Designated Agent Directory. Hægt er að verða sér úti um A P.O. pósthólfsskráningu með beinni beiðni; notaðu DMCA Designated Agent Post Office Box Waiver Request til að senda tölvupóst á Copyright Office og lýstu því yfir að þú sért heimavinnandi efnismiðlari (home-based content moderator) sem átt á hættu refsingar eða hefndir vegna þess sem þú miðlar og þurfir því á slíku pósthólfi að halda svo þitt eigið heimilisfang sé ekki gert opinbert.
-        dmca_email: Má vera sama tölvupóstfang og það sem notað er í “Tölvupóstfang vegna löglegra tilkynninga” hér að ofan
+        dmca_email: Má vera sama tölvupóstfang og það sem notað er í “Tölvupóstfang vegna löglegra tilkynninga” hér að ofan.
         domain: Einstakt auðkenni á netþjónustunni sem þú býður.
         jurisdiction: Settu inn landið þar sem sá býr sem borgar reikningana. Ef það er fyrirtæki eða samtök, skaltu hafa það landið þar sem lögheimili þess er, auk borgar, héraðs, svæðis eða fylkis eins og við á.
+        min_age: Ætti ekki að vera lægri en sá lágmarksaldur sek kveðið er á um í lögum þíns lögsagnarumdæmis.
       user:
         chosen_languages: Þegar merkt er við þetta, birtast einungis færslur á völdum tungumálum á opinberum tímalínum
+        date_of_birth: Við verðum að ganga úr skugga um að þú hafir náð %{age} aldri til að nota Mastodon. Við munum ekki geyma þessar upplýsingar.
         role: Hlutverk stýrir hvaða heimildir notandinn hefur.
       user_role:
         color: Litur sem notaður er fyrir hlutverkið allsstaðar í viðmótinu, sem RGB-gildi á hex-sniði
@@ -255,6 +261,7 @@ is:
         name: Myllumerki
       filters:
         actions:
+          blur: Fela myndefni með aðvörun
           hide: Fela alveg
           warn: Fela með aðvörun
       form_admin_settings:
@@ -268,6 +275,7 @@ is:
         favicon: Auðkennismynd
         mascot: Sérsniðið gæludýr (eldra)
         media_cache_retention_period: Tímalengd sem myndefni haldið
+        min_age: Kröfur um lágmarksaldur
         peers_api_enabled: Birta lista yfir uppgötvaða netþjóna í API-kerfisviðmótinu
         profile_directory: Virkja notendamöppu
         registrations_mode: Hverjir geta nýskráð sig
@@ -333,16 +341,22 @@ is:
         usable: Leyfa færslum að nota þetta myllumerki staðvært
       terms_of_service:
         changelog: Hvað breyttist?
+        effective_date: Gildistími
         text: Þjónustuskilmálar
       terms_of_service_generator:
         admin_email: Tölvupóstfang vegna löglegra tilkynninga
         arbitration_address: Raunverulegt heimilisfang fyrir tilkynningar um úrskurði
         arbitration_website: Vefsvæði til að senda inn tilkynningar um úrskurði
+        choice_of_law: Val á lagaumgjörð
         dmca_address: Raunverulegt heimilisfang fyrir kröfur vegna DMCA/höfundarréttar
         dmca_email: Tölvupóstfang tilkynninga vegna DMCA/höfundaréttar
         domain: Lén
         jurisdiction: Lögsagnarumdæmi
+        min_age: Lágmarksaldur
       user:
+        date_of_birth_1i: Dagur
+        date_of_birth_2i: Mánuður
+        date_of_birth_3i: Ár
         role: Hlutverk
         time_zone: Tímabelti
       user_role:
diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml
index 62fe75e838..da203270fa 100644
--- a/config/locales/simple_form.it.yml
+++ b/config/locales/simple_form.it.yml
@@ -75,6 +75,7 @@ it:
       filters:
         action: Scegli quale azione eseguire quando un post corrisponde al filtro
         actions:
+          blur: Nascondi i contenuti multimediali dietro un avviso, senza nascondere il testo stesso
           hide: Nascondi completamente il contenuto filtrato, come se non esistesse
           warn: Nascondi il contenuto filtrato e mostra invece un avviso, citando il titolo del filtro
       form_admin_settings:
@@ -88,6 +89,7 @@ it:
         favicon: WEBP, PNG, GIF o JPG. Sostituisce la favicon predefinita di Mastodon con un'icona personalizzata.
         mascot: Sostituisce l'illustrazione nell'interfaccia web avanzata.
         media_cache_retention_period: I file multimediali da post fatti da utenti remoti sono memorizzati nella cache sul tuo server. Quando impostato a un valore positivo, i media verranno eliminati dopo il numero specificato di giorni. Se i dati multimediali sono richiesti dopo che sono stati eliminati, saranno nuovamente scaricati, se il contenuto sorgente è ancora disponibile. A causa di restrizioni su quanto spesso link anteprima carte sondaggio siti di terze parti, si consiglia di impostare questo valore ad almeno 14 giorni, o le schede di anteprima link non saranno aggiornate su richiesta prima di quel tempo.
+        min_age: Gli utenti saranno invitati a confermare la loro data di nascita durante la registrazione
         peers_api_enabled: Un elenco di nomi di dominio che questo server ha incontrato nel fediverse. Qui non sono inclusi dati sul fatto se si federano con un dato server, solo che il server ne è a conoscenza. Questo viene utilizzato dai servizi che raccolgono statistiche sulla federazione in senso generale.
         profile_directory: La directory del profilo elenca tutti gli utenti che hanno acconsentito ad essere individuabili.
         require_invite_text: 'Quando le iscrizioni richiedono l''approvazione manuale, rendi la domanda: "Perché vuoi unirti?" obbligatoria anziché facoltativa'
@@ -132,17 +134,21 @@ it:
         name: Puoi cambiare solo il minuscolo/maiuscolo delle lettere, ad esempio, per renderlo più leggibile
       terms_of_service:
         changelog: Può essere strutturato con la sintassi Markdown.
+        effective_date: Un lasso di tempo ragionevole può variare da 10 a 30 giorni dalla data di notifica agli utenti.
         text: Può essere strutturato con la sintassi Markdown.
       terms_of_service_generator:
         admin_email: Gli avvisi legali includono controavvisi, ordinanze del tribunale, richieste di rimozione e richieste delle forze dell'ordine.
-        arbitration_address: Può essere lo stesso dell'indirizzo fisico sopra o "N/A" se si utilizza l'email
-        arbitration_website: Può essere un modulo web o "N/A" se si utilizza l'email
+        arbitration_address: Può essere uguale all'indirizzo fisico sopra indicato oppure "N/D" se si utilizza l'e-mail.
+        arbitration_website: Può essere un modulo web oppure "N/D" se si utilizza l'e-mail.
+        choice_of_law: Città, regione, territorio o Stato le cui leggi sostanziali interne regoleranno tutti i reclami.
         dmca_address: Per gli operatori statunitensi, utilizzare l'indirizzo registrato nella DMCA Designated Agent Directory. L'elenco delle caselle postali è disponibile su richiesta diretta, utilizzando il DMCA Designated Agent Post Office Box Waiver Request per inviare un'email al Copyright Office e descrivendo di essere un moderatore di contenuti che lavora da casa e che teme vendette o punizioni per le proprie azioni e che hai bisogno di usare una casella postale per rimuovere il tuo indirizzo di casa dalla vista pubblica.
-        dmca_email: Può essere lo stesso indirizzo email utilizzato per "Indirizzo email per avvisi legali" sopra
+        dmca_email: Può essere la stessa e-mail utilizzata per "Indirizzo e-mail per avvisi legali" sopra.
         domain: Identificazione univoca del servizio online che stai fornendo.
         jurisdiction: Indica il Paese in cui risiede il soggetto che paga le fatture. Se si tratta di un'azienda o di un altro ente, indicare il Paese in cui è costituito, nonché la città, la regione, il territorio o lo Stato, a seconda dei casi.
+        min_age: Non si dovrebbe avere un'età inferiore a quella minima richiesta, dalle leggi della tua giurisdizione.
       user:
         chosen_languages: Quando una o più lingue sono contrassegnate, nelle timeline pubbliche vengono mostrati solo i toot nelle lingue selezionate
+        date_of_birth: Dobbiamo verificare che tu abbia almeno %{age} anni per usare Mastodon. Non archivieremo questa informazione.
         role: Il ruolo controlla quali permessi ha l'utente.
       user_role:
         color: Colore da usare per il ruolo in tutta l'UI, come RGB in formato esadecimale
@@ -233,6 +239,7 @@ it:
         setting_display_media_show_all: Mostra tutti
         setting_expand_spoilers: Espandi sempre post con content warning
         setting_hide_network: Nascondi la tua rete
+        setting_missing_alt_text_modal: Chiedi di confermare prima di pubblicare media senza testo alternativo
         setting_reduce_motion: Riduci movimento nelle animazioni
         setting_system_font_ui: Usa il carattere predefinito del sistema
         setting_system_scrollbars_ui: Utilizza la barra di scorrimento predefinita del sistema
@@ -254,6 +261,7 @@ it:
         name: Etichetta
       filters:
         actions:
+          blur: Nascondi i contenuti multimediali con un avviso
           hide: Nascondi completamente
           warn: Nascondi con avviso
       form_admin_settings:
@@ -267,6 +275,7 @@ it:
         favicon: Favicon
         mascot: Personalizza mascotte (legacy)
         media_cache_retention_period: Periodo di conservazione della cache multimediale
+        min_age: Età minima richiesta
         peers_api_enabled: Pubblica l'elenco dei server scoperti nell'API
         profile_directory: Abilita directory del profilo
         registrations_mode: Chi può iscriversi
@@ -332,16 +341,22 @@ it:
         usable: Permetti ai post di utilizzare questo hashtag localmente
       terms_of_service:
         changelog: Cosa è cambiato?
+        effective_date: Data di decorrenza
         text: Termini di Servizio
       terms_of_service_generator:
         admin_email: Indirizzo email per avvisi legali
         arbitration_address: Indirizzo fisico per avvisi di arbitrato
         arbitration_website: Sito web per l'invio di avvisi di arbitrato
+        choice_of_law: Scelta della legge
         dmca_address: Indirizzo fisico per gli avvisi DMCA/copyright
         dmca_email: Indirizzo email per gli avvisi DMCA/copyright
         domain: Dominio
         jurisdiction: Giurisdizione legale
+        min_age: Età minima
       user:
+        date_of_birth_1i: Giorno
+        date_of_birth_2i: Mese
+        date_of_birth_3i: Anno
         role: Ruolo
         time_zone: Fuso orario
       user_role:
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index 18daa21214..b8728f63a0 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -3,6 +3,7 @@ ja:
   simple_form:
     hints:
       account:
+        attribution_domains: 1行につき1つずつ入力してください。この設定は関わりのないwebサイトに対して虚偽の帰属表示が行われることを防止する役割があります。
         discoverable: プロフィールと公開投稿をMastodonのおすすめやハイライトとしてほかのユーザーに表示することを許可します。
         display_name: フルネーム、ハンドルネームなど
         fields: ホームページ、代名詞、年齢など何でも構いません。
@@ -168,10 +169,7 @@ ja:
         text: Markdown 記法を利用できます。
       terms_of_service_generator:
         admin_email: 法的通知とは、異議申し立て通知、裁判所命令、削除要請、法執行機関による要請などをいいます。
-        arbitration_address: 上記住所と同じでもかまいません。電子メールを使用する場合は「N/A」とすることができます。
-        arbitration_website: ウェブフォームでもかまいません。電子メールを使用する場合は「N/A」とすることができます。
         dmca_address: 米国の運営者の場合は、DMCA Designated Agent Directory(DMCA指定代理人ディレクトリ)に登録のある住所を使用してください。申請を行えば記載を私書箱とすることも可能で、その場合はDMCA Designated Agent Post Office Box Waiver Request(DMCA指定代理人の郵便私書箱による免除申請)によって著作権局にメールを送信し、あなたが個人のコンテンツ管理者であって、行った措置に対して報復を受けるおそれがあり、住所を非公開とするために私書箱を使用する必要がある旨を伝えてください。
-        dmca_email: 上記「法的通知を受け取るメールアドレス」と同じものでもかまいません。
         domain: あなたの提供するこのオンラインサービスの識別名です。
         jurisdiction: 運営責任者が居住する国を記載します。企業や他の団体である場合は、その組織の所在国に加えて、市・区・州などの地域を記載します。
       user:
@@ -190,6 +188,7 @@ ja:
     kmyblue: kmyblue
     labels:
       account:
+        attribution_domains: あなたの著者表示を許可するwebサイト
         discoverable: アカウントを見つけやすくする
         fields:
           examples:
@@ -310,6 +309,7 @@ ja:
         setting_hide_quote_unavailable_server: 引用に対応していないと思われるサーバーの投稿からメニューを隠す
         setting_hide_status_reference_unavailable_server: ひかえめな引用(Fedibirdの参照)に対応していないと思われるサーバーの投稿からメニューを隠す
         setting_lock_follow_from_bot: botからのフォローを承認制にする
+        setting_missing_alt_text_modal: 代替テキストなしでメディアを投稿する前に確認ダイアログを表示する
         setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する
         setting_reduce_motion: アニメーションの動きを減らす
         setting_reject_public_unlisted_subscription: Misskey系サーバーに「ローカル公開」かつ検索許可「誰でも以外」の投稿を「フォロワーのみ」に変換して配送する
diff --git a/config/locales/simple_form.kab.yml b/config/locales/simple_form.kab.yml
index f390d42132..c0ff7e598e 100644
--- a/config/locales/simple_form.kab.yml
+++ b/config/locales/simple_form.kab.yml
@@ -18,7 +18,7 @@ kab:
         bot: Smekti-d wiyaḍ dakken amiḍan-a ixeddem s wudem amezwer tigawin tiwurmanin yernu ur yezmir ara ad yettwaɛass
         email: Ad n-teṭṭfeḍ imayl i usentem
         irreversible: Tisuffaɣ i tessazedgeḍ ad ttwakksent i lebda, ula ma tekkseḍ imsizdeg-nni ar zdat
-        locale: Tutlayt n ugrudem, imaylen d walɣuten yettudemren
+        locale: Tutlayt n ugrudem, imaylen d yilɣa yettudemren
         password: Seqdec ma drus 8 n yisekkilen
         setting_always_send_emails: S umata, ilɣa s yimayl ur d-ttwaceyyεen ara mi ara tesseqdaceḍ Mastodon s wudem urmid
         setting_display_media_default: Ffer imidyaten yettwacreḍ d infariyen
@@ -104,6 +104,7 @@ kab:
       featured_tag:
         name: Ahacṭag
       form_admin_settings:
+        app_icon: Tignit n usnas
         custom_css: CSS udmawan
         profile_directory: Rmed akaram n imaγnuten
         site_contact_email: Imayl n unermas
@@ -114,8 +115,8 @@ kab:
         theme: Asentel amezwer
         thumbnail: Tanfult n uqeddac
       interactions:
-        must_be_follower: Ssewḥel alɣuten sɣur wid akked tid ur yellin ara d imeḍfaren-ik·im
-        must_be_following: Ssewḥel alɣuten sɣur wid akked tid ur tettḍafareḍ ara
+        must_be_follower: Ssewḥel ilɣa sɣur wid akk d tid ur yellin ara d imeḍfaren-ik·im
+        must_be_following: Ssewḥel ilɣa sɣur wid akked tid ur tettḍafareḍ ara
         must_be_following_dm: Sewḥel iznan usriden sɣur wid akked tid ur tettḍafareḍ ara
       invite:
         comment: Awennit
@@ -140,6 +141,8 @@ kab:
         text: Alugen
       tag:
         name: Ahacṭag
+      terms_of_service:
+        text: Tiwtilin n useqdec
       user:
         role: Tamlilt
         time_zone: Tamnaḍt tasragant
diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml
index 92d306224a..661028fff0 100644
--- a/config/locales/simple_form.ko.yml
+++ b/config/locales/simple_form.ko.yml
@@ -75,6 +75,7 @@ ko:
       filters:
         action: 게시물이 필터에 걸러질 때 어떤 동작을 수행할 지 고르세요
         actions:
+          blur: 텍스트는 숨기지 않고 그대로 둔 채 경고 뒤에 미디어를 숨김니다
           hide: 필터에 걸러진 글을 처음부터 없었던 것처럼 완전히 가리기
           warn: 필터 제목을 언급하는 경고 뒤에 걸러진 내용을 숨기기
       form_admin_settings:
@@ -88,6 +89,7 @@ ko:
         favicon: WEBP, PNG, GIF 또는 JPG. 기본 파비콘을 대체합니다.
         mascot: 고급 웹 인터페이스의 그림을 대체합니다.
         media_cache_retention_period: 원격 사용자가 작성한 글의 미디어 파일은 이 서버에 캐시됩니다. 양수로 설정하면 지정된 일수 후에 미디어가 삭제됩니다. 삭제된 후에 미디어 데이터를 요청하면 원본 콘텐츠를 사용할 수 있는 경우 다시 다운로드됩니다. 링크 미리 보기 카드가 타사 사이트를 폴링하는 빈도에 제한이 있으므로 이 값을 최소 14일로 설정하는 것이 좋으며, 그렇지 않으면 그 이전에는 링크 미리 보기 카드가 제때 업데이트되지 않을 것입니다.
+        min_age: 사용자들은 가입할 때 생일을 확인받게 됩니다
         peers_api_enabled: 이 서버가 연합우주에서 만났던 서버들에 대한 도메인 네임의 목록입니다. 해당 서버와 어떤 연합을 했는지에 대한 정보는 전혀 포함되지 않고, 단순히 그 서버를 알고 있는지에 대한 것입니다. 이것은 일반적으로 연합에 대한 통계를 수집할 때 사용됩니다.
         profile_directory: 프로필 책자는 발견되기를 희망하는 모든 사람들의 목록을 나열합니다.
         require_invite_text: 가입이 수동 승인을 필요로 할 때, "왜 가입하려고 하나요?" 항목을 선택사항으로 두는 것보다는 필수로 두는 것이 낫습니다
@@ -132,16 +134,19 @@ ko:
         name: 읽기 쉽게하기 위한 글자의 대소문자만 변경할 수 있습니다.
       terms_of_service:
         changelog: 마크다운 문법을 사용할 수 있습니다.
+        effective_date: 사용자에게 통지한 날로부터 10일에서 30일 사이가 합리적인 기간입니다.
         text: 마크다운 문법을 사용할 수 있습니다.
       terms_of_service_generator:
         admin_email: 법적 고지에는 이의 제기, 법원 명령, 게시 중단 요청, 법 집행 요청이 포함됩니다.
-        arbitration_address: 위의 실제 주소와 같을 수 있으며, 이메일을 사용한다면 "N/A"로 두세요
-        arbitration_website: 웹 형태를 사용할 수 있습니다. 이메일을 사용한다면 "N/A"로 둘 수 있습니다
-        dmca_email: 상단의 "법적 통지를 위한 이메일 주소"와 같은 주소를 사용할 수 있습니다
+        arbitration_address: 위의 실제 주소와 같을 수 있으며, 이메일을 사용한다면 "N/A"로 두세요.
+        arbitration_website: 웹 형태를 사용할 수 있습니다. 이메일을 사용한다면 "N/A"로 둘 수 있습니다.
+        dmca_email: 상단의 "법적 통지를 위한 이메일 주소"와 같은 주소를 사용할 수 있습니다.
         domain: 귀하가 제공하는 온라인 서비스의 고유 식별정보입니다.
         jurisdiction: 요금을 지불하는 사람이 거주하는 국가를 기재하세요. 회사나 기타 법인인 경우 해당 법인이 설립된 국가와 도시, 지역, 영토 또는 주를 적절히 기재하세요.
+        min_age: 관할지역의 법률에서 요구하는 최저 연령보다 작으면 안 됩니다.
       user:
         chosen_languages: 체크하면, 선택 된 언어로 작성된 게시물들만 공개 타임라인에 보여집니다
+        date_of_birth: 마스토돈을 사용하려면 %{age}세 이상임을 확인해야 합니다. 이 정보는 저장되지 않습니다.
         role: 역할은 사용자가 어떤 권한을 가지게 될 지 결정합니다.
       user_role:
         color: 색상은 사용자 인터페이스에서 역할을 나타내기 위해 사용되며, RGB 16진수 형식입니다
@@ -151,7 +156,7 @@ ko:
         position: 특정 상황에서 충돌이 발생할 경우 더 높은 역할이 충돌을 해결합니다. 특정 작업은 우선순위가 낮은 역할에 대해서만 수행될 수 있습니다
       webhook:
         events: 전송할 이벤트를 선택하세요
-        template: 원하는 JSON 페이로드를 변수와 함께 작성하거나, 그냥 냅둬서 기본 JSON을 사용할 수 있습니다.
+        template: 원하는 JSON 페이로드를 변수와 함께 작성하거나, 그대로 두어 기본 JSON을 사용할 수 있습니다.
         url: 이벤트가 어디로 전송될 지
     labels:
       account:
@@ -219,11 +224,11 @@ ko:
         setting_aggregate_reblogs: 타임라인의 부스트를 그룹화
         setting_always_send_emails: 항상 이메일 알림 보내기
         setting_auto_play_gif: 애니메이션 GIF를 자동 재생
-        setting_boost_modal: 부스트 전 확인 창을 표시
+        setting_boost_modal: 부스트 전 확인창을 띄웁니다
         setting_default_language: 게시물 언어
         setting_default_privacy: 게시물 프라이버시
         setting_default_sensitive: 미디어를 언제나 민감한 콘텐츠로 설정
-        setting_delete_modal: 게시물 삭제 전 확인 창을 표시
+        setting_delete_modal: 게시물 삭제 전 확인창을 띄웁니다
         setting_disable_hover_cards: 호버시 프로필 미리보기를 비활성화
         setting_disable_swiping: 스와이프 모션 비활성화
         setting_display_media: 미디어 표시
@@ -232,6 +237,7 @@ ko:
         setting_display_media_show_all: 모두 보이기
         setting_expand_spoilers: 내용 경고로 표시된 게시물을 항상 펼치기
         setting_hide_network: 내 인맥 숨기기
+        setting_missing_alt_text_modal: 대체 텍스트 없이 미디어를 게시하려고 할 때 확인창을 띄웁니다
         setting_reduce_motion: 애니메이션 줄이기
         setting_system_font_ui: 시스템의 기본 글꼴을 사용
         setting_system_scrollbars_ui: 시스템 기본 스크롤바 사용
@@ -253,6 +259,7 @@ ko:
         name: 해시태그
       filters:
         actions:
+          blur: 경고와 함께 미디어 숨기기
           hide: 완전히 숨기기
           warn: 경고와 함께 숨기기
       form_admin_settings:
@@ -266,6 +273,7 @@ ko:
         favicon: 파비콘
         mascot: 사용자 정의 마스코트 (legacy)
         media_cache_retention_period: 미디어 캐시 유지 기한
+        min_age: 최소 연령 제한
         peers_api_enabled: API에 발견 된 서버들의 목록 발행
         profile_directory: 프로필 책자 활성화
         registrations_mode: 누가 가입할 수 있는지
@@ -331,16 +339,22 @@ ko:
         usable: 이 해시태그를 로컬 게시물에서 사용 가능하도록 허용
       terms_of_service:
         changelog: 무엇이 바뀌었나요?
+        effective_date: 시행일
         text: 이용 약관
       terms_of_service_generator:
         admin_email: 법적 조치를 위한 이메일 주소
         arbitration_address: 중재 통지를 위한 실제 주소
         arbitration_website: 중재 통지를 제출하기 위한 웹사이트
+        choice_of_law: 준거법 지정
         dmca_address: DMCA/저작권 통지를 위한 실제 주소
         dmca_email: DMCA/저작권 통지를 위한 이메일 주소
         domain: 도메인
         jurisdiction: 법적 관할권
+        min_age: 최소 연령
       user:
+        date_of_birth_1i: 일
+        date_of_birth_2i: 월
+        date_of_birth_3i: 년
         role: 역할
         time_zone: 시간대
       user_role:
diff --git a/config/locales/simple_form.lad.yml b/config/locales/simple_form.lad.yml
index de37005312..a3f70bd361 100644
--- a/config/locales/simple_form.lad.yml
+++ b/config/locales/simple_form.lad.yml
@@ -308,7 +308,12 @@ lad:
         name: Etiketa
         trendable: Permite ke esta etiketa apareska en trendes
         usable: Permite ke publikasyones uzen esta etiketa lokalmente
+      terms_of_service_generator:
+        domain: Domeno
       user:
+        date_of_birth_1i: Diya
+        date_of_birth_2i: Mez
+        date_of_birth_3i: Anyo
         role: Rolo
         time_zone: Zona de tiempo
       user_role:
diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml
index 73460d2667..d3febceea9 100644
--- a/config/locales/simple_form.lt.yml
+++ b/config/locales/simple_form.lt.yml
@@ -72,6 +72,7 @@ lt:
       filters:
         action: Pasirink, kokį veiksmą atlikti, kai įrašas atitinka filtrą
         actions:
+          blur: Slėpti mediją po įspėjimu, neslepiant paties teksto
           hide: Visiškai paslėpti filtruotą turinį ir elgtis taip, tarsi jo neegzistuotų
           warn: Slėpti filtruojamą turinį po įspėjimu, paminint filtro pavadinimą
       form_admin_settings:
@@ -82,6 +83,7 @@ lt:
         favicon: WEBP, PNG, GIF arba JPG. Pakeičia numatytąją Mastodon svetaines piktogramą pasirinktine piktograma.
         mascot: Pakeičia išplėstinės žiniatinklio sąsajos iliustraciją.
         media_cache_retention_period: Nuotolinių naudotojų įrašytų įrašų medijos failai talpinami tavo serveryje. Nustačius teigiamą reikšmę, medijos bus ištrinamos po nurodyto dienų skaičiaus. Jei medijos duomenų bus paprašyta po to, kai jie bus ištrinti, jie bus atsiųsti iš naujo, jei šaltinio turinys vis dar prieinamas. Dėl apribojimų, susijusių su nuorodų peržiūros kortelių apklausos dažnumu trečiųjų šalių svetainėse, rekomenduojama nustatyti šią reikšmę ne trumpesnę kaip 14 dienų, kitaip nuorodų peržiūros kortelės nebus atnaujinamos pagal pareikalavimą iki to laiko.
+        min_age: Registracijos metu naudotojai bus paprašyti patvirtinti savo gimimo datą.
         peers_api_enabled: Domenų pavadinimų sąrašas, su kuriais šis serveris susidūrė fediverse. Čia nėra duomenų apie tai, ar tu bendrauji su tam tikru serveriu, tik apie tai, kad tavo serveris apie jį žino. Tai naudojama tarnybose, kurios renka federacijos statistiką bendrąja prasme.
         require_invite_text: Kai registraciją reikia patvirtinti rankiniu būdu, teksto įvesties laukelį „Kodėl nori prisijungti?“ padaryk privalomą, o ne pasirenkamą
         site_contact_email: Kaip žmonės gali su tavimi susisiekti teisiniais ar pagalbos užklausimais.
@@ -110,12 +112,15 @@ lt:
         admin_email: Teisiniai pranešimai įtraukia priešpriešinius pranešimus, teismo įsakymus, pašalinimo prašymus ir teisėsaugos institucijų prašymus.
         arbitration_address: Gali būti toks pat kaip aukščiau nurodytas fizinis adresas arba „N/A“ (netaikoma), jei naudojamas el. paštas.
         arbitration_website: Gali būti interneto forma arba „N/A“ (netaikoma), jei naudojamas el. paštas.
+        choice_of_law: Miestas, regionas, teritorija ar valstija, kurių vidaus materialinė teisė reglamentuoja visus reikalavimus.
         dmca_address: JAV operatoriams naudokite DMCA paskirtojo agento kataloge užregistruotą adresą. Pašto dėžutės sąrašą galima sudaryti pateikus tiesioginį prašymą, naudokite DMCA paskirtojo agento pašto dėžutės atsisakymo prašymą, kad parašytumėte el. laišką Autorinių teisių tarnybai ir aprašytumėte, kad esate namuose įsikūręs turinio moderatorius, kuris baiminasi keršto ar bausmės už savo veiksmus ir kuriam reikia naudoti pašto dėžutę, kad jo namų adresas nebūtų viešai matomas.
         dmca_email: Gali būti tas pats aukščiau nurodytas el. pašto adresas, naudojamas „El. pašto adresas, skirtas teisiniams pranešimams“.
         domain: Unikalus jūsų teikiamos internetinės paslaugos identifikavimas.
         jurisdiction: Nurodykite šalį, kurioje gyvena tas, kas apmoka sąskaitas. Jei tai bendrovė ar kita esybė, nurodykite šalį, kurioje jis įregistruotas, ir atitinkamai miestą, regioną, teritoriją ar valstiją.
+        min_age: Neturėtų būti žemiau mažiausio amžiaus, reikalaujamo pagal jūsų jurisdikcijos įstatymus.
       user:
         chosen_languages: Kai pažymėta, viešose laiko skalėse bus rodomi tik įrašai pasirinktomis kalbomis.
+        date_of_birth: Turime įsitikinti, kad esate bent %{age}, kad naudotumėte „Mastodon“. Mes to neišsaugosime.
         role: Vaidmuo valdo, kokius leidimus naudotojas turi.
     labels:
       account:
@@ -160,6 +165,7 @@ lt:
         setting_display_media_show_all: Rodyti viską
         setting_expand_spoilers: Visada išplėsti įrašus, pažymėtus turinio įspėjimais
         setting_hide_network: Slėpti savo socialinę diagramą
+        setting_missing_alt_text_modal: Rodyti patvirtinimo dialogo langą prieš skelbiant mediją be alternatyvaus teksto.
         setting_reduce_motion: Sumažinti judėjimą animacijose
         setting_system_font_ui: Naudoti numatytąjį sistemos šriftą
         setting_system_scrollbars_ui: Naudoti numatytąją sistemos slankjuostę
@@ -178,6 +184,7 @@ lt:
         name: Saitažodis
       filters:
         actions:
+          blur: Slėpti mediją su įspėjimu
           hide: Slėpti visiškai
           warn: Slėpti su įspėjimu
       form_admin_settings:
@@ -188,6 +195,7 @@ lt:
         custom_css: Pasirinktinis CSS
         favicon: Svetainės piktograma
         mascot: Pasirinktinis talismanas (pasenęs)
+        min_age: Mažiausias amžiaus reikalavimas
         registrations_mode: Kas gali užsiregistruoti
         require_invite_text: Reikalauti priežasties prisijungti
         show_domain_blocks_rationale: Rodyti, kodėl domenai buvo užblokuoti
@@ -233,11 +241,16 @@ lt:
         admin_email: El. pašto adresas, skirtas teisiniams pranešimams
         arbitration_address: Fizinis adresas pranešimams apie arbitražą
         arbitration_website: Svetainė pranešimams apie arbitražą
+        choice_of_law: Teisės pasirinkimas
         dmca_address: Fizinis adresas, skirtas DMCA / autorinių teisių pranešimams
         dmca_email: El. pašto adresas, skirtas DMCA / autorinių teisių pranešimams
         domain: Domenas
         jurisdiction: Teisinis teismingumas
+        min_age: Mažiausias amžius
       user:
+        date_of_birth_1i: Diena
+        date_of_birth_2i: Mėnuo
+        date_of_birth_3i: Metai
         role: Vaidmuo
         time_zone: Laiko juosta
       user_role:
diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml
index 85cc38fa9e..287ce36a5d 100644
--- a/config/locales/simple_form.lv.yml
+++ b/config/locales/simple_form.lv.yml
@@ -3,6 +3,7 @@ lv:
   simple_form:
     hints:
       account:
+        attribution_domains: Viens katrā līnijā. Aizsargā no nepatiesa attiecinājuma.
         discoverable: Tavas publiskās ziņas un profils var tikt piedāvāti vai ieteikti dažādās Mastodon vietās, un tavs profils var tikt ieteikts citiem lietotājiem.
         display_name: Tavs pilnais vārds vai tavs joku vārds.
         fields: Tava mājas lapa, vietniekvārdi, vecums, viss, ko vēlies.
@@ -24,8 +25,8 @@ lv:
         type_html: Izvēlies, ko darīt ar <strong>%{acct}</strong>
         types:
           disable: Neļauj lietotājam izmantot savu kontu, bet neizdzēs vai neslēp tā saturu.
-          none: Izmanto šo, lai nosūtītu lietotājam brīdinājumu, neradot nekādas citas darbības.
-          sensitive: Piespiest visus šī lietotāja multivides pielikumus atzīmēt kā sensitīvus.
+          none: Šis ir izmantojams, lai nosūtītu lietotājam brīdinājumu bez jebkādu citu darbību izraisīšanas.
+          sensitive: Visus šī lietotāja informācijas nesēju pielikumus uzspiesti atzīmēt kā jūtīgus.
           silence: Neļaut lietotājam veikt ierakstus ar publisku redzamību, paslēpt viņa ierakstus un paziņojumus no cilvēkiem, kas tam neseko. Tiek aizvērti visi ziņojumi par šo kontu.
           suspend: Novērs jebkādu mijiedarbību no šī konta vai uz to un dzēs tā saturu. Atgriežams 30 dienu laikā. Tiek aizvērti visi šī konta pārskati.
         warning_preset_id: Neobligāts. Tu joprojām vari pievienot pielāgotu tekstu sākotnējās iestatīšanas beigās
@@ -44,8 +45,8 @@ lv:
         context: Viens vai vairāki konteksti, kur jāpiemēro filtrs
         current_password: Drošības nolūkos, lūdzu, ievadi pašreizējā konta paroli
         current_username: Lai apstiprinātu, lūdzu, ievadi pašreizējā konta paroli
-        digest: Tiek nosūtīts tikai pēc ilgstošas bezdarbības un tikai tad, ja savas prombūtnes laikā esi saņēmis jebkādas personīgas ziņas
-        email: Tev tiks nosūtīts apstiprinājuma e-pasts
+        digest: Tiek nosūtīts tikai pēc ilgstošas bezdarbības un tikai tad, ja savas prombūtnes laikā saņēmi jebkādas personīgas ziņas
+        email: Tev tiks nosūtīts apstiprinājuma e-pasta ziņojums
         header: WEBP, PNG, GIF vai JPG. Ne vairāk kā %{size}. Tiks samazināts līdz %{dimensions}px
         inbox_url: Nokopē URL no tā releja sākumlapas, kuru vēlies izmantot
         irreversible: Filtrētās ziņas neatgriezeniski pazudīs, pat ja filtrs vēlāk tiks noņemts
@@ -55,10 +56,11 @@ lv:
         scopes: Kuriem API lietotnei būs ļauts piekļūt. Ja atlasa augstākā līmeņa tvērumu, nav nepieciešamas atlasīt atsevišķus.
         setting_aggregate_reblogs: Nerādīt jaunus izcēlumus ziņām, kas nesen tika palielinātas (ietekmē tikai nesen saņemtos palielinājumus)
         setting_always_send_emails: Parasti e-pasta paziņojumi netiek sūtīti, kad aktīvi izmantojat Mastodon
-        setting_default_sensitive: Sensitīva multivide pēc noklusējuma ir paslēpti, un tos var atklāt, noklikšķinot
-        setting_display_media_default: Paslēpt multividi, kas atzīmēta kā sensitīva
+        setting_default_sensitive: Pēc noklusējuma jūtīgi informācijas nesēji ir paslēpti, un tos var atklāt ar klikšķi
+        setting_display_media_default: Paslēpt informācijas nesējus, kas atzīmēti kā jūtīgi
         setting_display_media_hide_all: Vienmēr slēpt multividi
         setting_display_media_show_all: Vienmēr rādīt multividi
+        setting_system_scrollbars_ui: Attiecas tikai uz darbvirsmas pārlūkiem, kuru pamatā ir Safari vai Chrome
         setting_use_blurhash: Pāreju pamatā ir paslēpto uzskatāmo līdzekļu krāsas, bet saturs tiek padarīts neskaidrs
         setting_use_pending_items: Paslēpt laika skalas atjauninājumus aiz klikšķa, nevis ar automātisku plūsmas ritināšanu
         username: Tu vari lietot burtus, ciparus un zemsvītras
@@ -73,19 +75,21 @@ lv:
       filters:
         action: Izvēlies, kuru darbību veikt, ja ziņa atbilst filtram
         actions:
+          blur: Paslēpt informācijas nesējus aiz brīdinājuma, nepaslēpjot tekstu
           hide: Paslēp filtrēto saturu pilnībā, izturoties tā, it kā tas neeksistētu
           warn: Paslēp filtrēto saturu aiz brīdinājuma, kurā minēts filtra nosaukums
       form_admin_settings:
         activity_api_enabled: Vietēji publicēto ziņu, aktīvo lietotāju un jauno reģistrāciju skaits nedēļas kopās
         app_icon: WEBP, PNG, GIF vai JPG. Mobilajās ierīcēs aizstāj noklusējuma lietotnes ikonu ar pielāgotu.
         backups_retention_period: Lietotājiem ir iespēja izveidot savu ierakstu arhīvu lejupielādēšanai vēlāk. Kad iestatīta pozitīva vērtība, šie arhīvi tiks automātiski izdzēsti no krātuves pēc norādītā dienu skaita.
-        bootstrap_timeline_accounts: Šie konti tiks piesprausti jauno lietotāju ieteikumu augšdaļā.
+        bootstrap_timeline_accounts: Šie konti tiks piesprausti jauno lietotāju sekošanas ieteikumu augšdaļā.
         closed_registrations_message: Tiek rādīts, kad reģistrēšanās ir slēgta
         content_cache_retention_period: Visi ieraksti no citiem serveriem (tajā skaitā pastiprinājumi un atbildes) tiks izdzēsti pēc norādītā dienu skaita, neņemot vērā vietēja lietotāja mijiedarbību ar šādiem ierakstiem. Tas ietver ierakstus, kurus vietējs lietotājs ir atzīmējis kā grāmatzīmi vai pievienojis izlasē. Tiks zaudēti arī privāti pieminējumi starp lietotājiem no dažādiem serveriem, un tos nebūs iespējams atgūt. Šī iestatījuma izmantošana ir paredzēta īpašam nolūkam paredzētiem serveriem un neatbilst tam, ko sagaida vairums lietotāju, kad pielietots vispārējas izmantošanas serveros.
         custom_css: Vari lietot pielāgotus stilus Mastodon tīmekļa versijā.
         favicon: WEBP, PNG, GIF vai JPG. Aizstāj noklusējuma Mastodon favikonu ar pielāgotu.
         mascot: Ignorē ilustrāciju uzlabotajā tīmekļa saskarnē.
         media_cache_retention_period: Informācijas nesēju datnes no ierakstiem, kurus ir veikuši attālie lietotāji, tiek kešoti šajā serverī. Kad ir iestatīta apstiprinoša vērtība, informācijas nesēji tiks izdzēsti pēc norādītā dienu skaita. Ja informācijas nesēju dati tiks pieprasīti pēc tam, kad tie tika izdzēsti, tie tiks atkārtoti lejupielādēti, ja avota saturs joprojām būs pieejams. Saišu priekšskatījuma karšu vaicājumu biežuma ierobežojumu dēļ ir ieteicams iestatīt šo vērtību vismaz 14 dienas vai saišu priekšskatījuma kartes netiks atjauninātas pēc pieprasījuma pirms tā laika.
+        min_age: Lietotājiem tiks lūgts apstiprināt viņu dzimšanas datumu reģistrācijas laikā
         peers_api_enabled: Domēna vārdu saraksts, ar kuriem šis serveris ir saskāries fediversā. Šeit nav iekļauti dati par to, vai tu veic federāciju ar noteiktu serveri, tikai tavs serveris par to zina. To izmanto dienesti, kas apkopo statistiku par federāciju vispārīgā nozīmē.
         profile_directory: Profilu direktorijā ir uzskaitīti visi lietotāji, kuri ir izvēlējušies būt atklājami.
         require_invite_text: Ja nepieciešama pašrocīga apstiprināšana, lai pierakstītos, teksta “Kāpēc vēlies pievienoties?” ievade jāpadara par nepieciešamu, nevis izvēles
@@ -121,15 +125,24 @@ lv:
         hint: Izvēles. Sniedz vairāk informācijas par noteikumu
         text: Jāapraksta nosacījums vai prasība šī servera lietotājiem. Jāmēģina to veidot īsu un vienkāršu
       sessions:
-        otp: 'Ievadi divfaktoru kodu, ko ģenerējusi tava tālruņa lietotne, vai izmanto kādu no atkopšanas kodiem:'
+        otp: 'Jāievada tālruņa lietotnes izveidots divpakāpju kods vai jāizmanto viens no saviem atkopes kodiem:'
         webauthn: Ja tā ir USB atslēga, noteikti ievieto to un, ja nepieciešams, pieskaries tai.
       settings:
         indexable: Tava profila lapa var tikt parādīta Google, Bing un citu meklēšanas dzinēju rezultātos.
         show_application: Tu vienmēr varēsi redzēt, kura lietotne publicēja tavu ziņu.
       tag:
         name: Tu vari mainīt tikai burtu lielumu, piemēram, lai tie būtu vieglāk lasāmi
+      terms_of_service:
+        changelog: Var veidot ar Markdown pierakstu.
+        effective_date: Saprātīgs laika logs var būt no 10 līdz 30 dienām no dienas, kad lietotāji tiek apziņoti.
+        text: Var veidot ar Markdown pierakstu.
+      terms_of_service_generator:
+        arbitration_address: Var būt tāda pati kā augstāk esošā fiziskā adrese vai "N/A", ja tiek izmantota e-pasta adrese.
+        arbitration_website: Var būt tīmekļa veidlapa vai "N/A", ja tiek izmantots e-pasts.
+        domain: Sniegtā tiešsaistas pakalpojuma neatkārtojama identifikācija.
       user:
         chosen_languages: Ja ieķeksēts, publiskos laika grafikos tiks parādītas tikai ziņas noteiktajās valodās
+        date_of_birth: Mums jāpārliecinās, ka jums ir vismaz %{age} gadi, lai varētu izmantot Mastodonu. Mēs neuzglabāsim šo informāciju.
         role: Loma nosaka, kādas lietotājam ir atļaujas.
       user_role:
         color: Krāsa, kas jāizmanto lomai visā lietotāja saskarnē, kā RGB hex formātā
@@ -143,6 +156,7 @@ lv:
         url: Kur notikumi tiks nosūtīti
     labels:
       account:
+        attribution_domains: Tīmekļvietnes, kurām ir tiesības uzskaitīt Tevi
         discoverable: Funkcijas profils un ziņas atklāšanas algoritmos
         fields:
           name: Marķējums
@@ -165,7 +179,7 @@ lv:
         types:
           disable: Iesaldēt
           none: Nosūtīt brīdinājumu
-          sensitive: Sensitīvs
+          sensitive: Jūtīgs
           silence: Ierobežot
           suspend: Apturēt
         warning_preset_id: Lietot iepriekš iestatītus brīdinājumus
@@ -199,7 +213,7 @@ lv:
         max_uses: Maksimālais lietojumu skaits
         new_password: Jauna parole
         note: Par sevi
-        otp_attempt: Divfaktoru kods
+        otp_attempt: Divpakāpju kods
         password: Parole
         phrase: Atslēgvārds vai frāze
         setting_advanced_layout: Iespējot paplašināto tīmekļa saskarni
@@ -209,7 +223,7 @@ lv:
         setting_boost_modal: Rādīt apstiprinājuma dialogu pirms izcelšanas
         setting_default_language: Publicēšanas valoda
         setting_default_privacy: Publicēšanas privātums
-        setting_default_sensitive: Atļaut atzīmēt multividi kā sensitīvu
+        setting_default_sensitive: Vienmēr atzīmēt informācijas nesējus kā jūtīgus
         setting_delete_modal: Parādīt apstiprinājuma dialogu pirms ziņas dzēšanas
         setting_disable_hover_cards: Atspējot profila priekšskatījumu pēc kursora novietošanas
         setting_disable_swiping: Atspējot vilkšanas kustības
@@ -219,8 +233,10 @@ lv:
         setting_display_media_show_all: Parādīt visu
         setting_expand_spoilers: Vienmēr izvērst ziņas, kas apzīmētas ar brīdinājumiem par saturu
         setting_hide_network: Slēpt savu sociālo grafu
+        setting_missing_alt_text_modal: Rādīt apstiprināšanas lodziņu pirms informācijas nesēju bez aprakstošā teksta iesūtīšanas
         setting_reduce_motion: Ierobežot kustību animācijās
         setting_system_font_ui: Lietot sistēmas noklusējuma fontu
+        setting_system_scrollbars_ui: Lietot sistēmas noklusējuma ritjoslu
         setting_theme: Vietnes motīvs
         setting_trends: Parādīt šodienas tendences
         setting_unfollow_modal: Parādīt apstiprinājuma dialogu pirms pārtraukt kādam sekot
@@ -252,6 +268,7 @@ lv:
         favicon: Favikona
         mascot: Pielāgots talismans (mantots)
         media_cache_retention_period: Multivides kešatmiņas saglabāšanas periods
+        min_age: Nepieciešamais minimālais vecums
         peers_api_enabled: Publicēt API atklāto serveru sarakstu
         profile_directory: Iespējot profila direktoriju
         registrations_mode: Kurš drīkst pieteikties
@@ -317,10 +334,17 @@ lv:
         usable: Ļaut ierakstos vietēji izmantot šo tēmturi
       terms_of_service:
         changelog: Kas ir mainījies?
+        effective_date: Spēkā stāšanās datums
         text: Pakalpojuma izmantošanas nosacījumi
       terms_of_service_generator:
+        admin_email: E-pasta adrese juridiskiem paziņojumiem
+        choice_of_law: Likuma izvēle
         domain: Domēna vārds
+        min_age: Mazākais pieļaujamais vecums
       user:
+        date_of_birth_1i: Diena
+        date_of_birth_2i: Mēnesis
+        date_of_birth_3i: Gads
         role: Loma
         time_zone: Laika josla
       user_role:
diff --git a/config/locales/simple_form.ms.yml b/config/locales/simple_form.ms.yml
index 96c6002aa4..d3670a1c86 100644
--- a/config/locales/simple_form.ms.yml
+++ b/config/locales/simple_form.ms.yml
@@ -77,6 +77,7 @@ ms:
         bootstrap_timeline_accounts: Akaun ini akan disematkan pada bahagian atas cadangan ikutan pengguna baharu.
         closed_registrations_message: Dipaparkan semasa pendaftaran ditutup
         custom_css: Anda boleh menggunakan gaya tersuai pada versi web Mastodon.
+        favicon: WEBP, PNG, GIF, atau JPG. Arca tersuai diutamakan dari arca kegemaran Mastodon lalai.
         mascot: Mengatasi ilustrasi dalam antara muka web lanjutan.
         peers_api_enabled: Senarai nama domain yang pernah ditemui oleh server ini dalam fediverse. Tiada data disertakan di sini tentang sama ada anda bersekutu dengan server tertentu, cuma server anda mengetahuinya. Ini digunakan oleh perkhidmatan yang mengumpul statistik mengenai persekutuan dalam pengertian umum.
         profile_directory: Direktori profil menyenaraikan semua pengguna yang telah mengikut serta untuk ditemui.
@@ -119,6 +120,8 @@ ms:
         show_application: Anda akan sentiasa dapat melihat apl yang menerbitkan siaran anda tanpa mengira.
       tag:
         name: Anda hanya boleh menukar selongsong huruf, sebagai contoh, untuk menjadikannya lebih mudah dibaca
+      terms_of_service_generator:
+        choice_of_law: City, region, territory or state the internal substantive laws of which shall govern any and all claims.
       user:
         chosen_languages: Apabila disemak, hanya siaran dalam bahasa terpilih akan dipaparkan dalam garis masa awam
       user_role:
@@ -236,6 +239,7 @@ ms:
         bootstrap_timeline_accounts: Sentiasa mengesyorkan akaun ini kepada pengguna baharu
         closed_registrations_message: Mesej tersuai apabila pendaftaran tidak tersedia
         custom_css: CSS tersuai
+        favicon: Arca kegemaran
         mascot: Maskot tersuai (warisan)
         media_cache_retention_period: Tempoh pengekalan cache media
         peers_api_enabled: Terbitkan senarai pelayan yang ditemui dalam API
@@ -276,7 +280,7 @@ ms:
       notification_emails:
         appeal: Seseorang merayu keputusan moderator
         digest: Hantar e-mel ringkasan
-        favourite: Seorang menggemarkan hantaran anda
+        favourite: Seseorang menyukai hantaran anda
         follow: Seorang mengikuti anda
         follow_request: Seorang meminta untuk mengikuti anda
         mention: Seorang menyebut anda
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
index 7079b25c6b..6029698bd7 100644
--- a/config/locales/simple_form.nl.yml
+++ b/config/locales/simple_form.nl.yml
@@ -75,6 +75,7 @@ nl:
       filters:
         action: Kies welke acties uitgevoerd moeten wanneer een bericht overeenkomt met het filter
         actions:
+          blur: Media verbergen achter een waarschuwing, zonder de tekst zelf te verbergen
           hide: Verberg de gefilterde inhoud volledig, alsof het niet bestaat
           warn: Verberg de gefilterde inhoud achter een waarschuwing, met de titel van het filter als waarschuwingstekst
       form_admin_settings:
@@ -88,6 +89,7 @@ nl:
         favicon: WEBP, PNG, GIF of JPG. Vervangt de standaard Mastodon favicon met een aangepast pictogram.
         mascot: Overschrijft de illustratie in de geavanceerde webomgeving.
         media_cache_retention_period: Mediabestanden van berichten van externe gebruikers worden op jouw server in de cache opgeslagen. Indien ingesteld op een positieve waarde, worden media verwijderd na het opgegeven aantal dagen. Als de mediagegevens worden opgevraagd nadat ze zijn verwijderd, worden ze opnieuw gedownload wanneer de originele inhoud nog steeds beschikbaar is. Vanwege beperkingen op hoe vaak linkvoorbeelden sites van derden raadplegen, wordt aanbevolen om deze waarde in te stellen op ten minste 14 dagen. Anders worden linkvoorbeelden niet op aanvraag bijgewerkt.
+        min_age: Gebruikers krijgen tijdens hun inschrijving de vraag om hun geboortedatum te bevestigen
         peers_api_enabled: Een lijst met domeinnamen die deze server heeft aangetroffen in de fediverse. Er zijn hier geen gegevens inbegrepen over de vraag of je verbonden bent met een bepaalde server, alleen dat je server er van weet. Dit wordt gebruikt door diensten die statistieken over de federatie in algemene zin verzamelen.
         profile_directory: De gebruikersgids bevat een lijst van alle gebruikers die ervoor gekozen hebben om ontdekt te kunnen worden.
         require_invite_text: Maak het invullen van "Waarom wil je je hier registreren?" verplicht in plaats van optioneel, wanneer registraties handmatig moeten worden goedgekeurd
@@ -132,17 +134,21 @@ nl:
         name: Je kunt elk woord met een hoofdletter beginnen, om zo bijvoorbeeld de tekst leesbaarder te maken
       terms_of_service:
         changelog: Kan worden opgemaakt met Markdown.
+        effective_date: Een redelijke periode kan variëren van 10 tot 30 dagen vanaf de datum waarop je jouw gebruikers op de hoogte stelt.
         text: Kan worden opgemaakt met Markdown.
       terms_of_service_generator:
         admin_email: Juridische mededelingen zijn o. a. counter-notices, gerechterlijke bevelen, takedown-requests en handhavingsverzoeken.
-        arbitration_address: Kan hetzelfde zijn als bovenstaand vestigingsadres of "N/A" bij gebruik van e-mail
-        arbitration_website: Kan een webformulier zijn, of "N/A" als e-mail wordt gebruikt
+        arbitration_address: Kan hetzelfde zijn als bovenstaand vestigingsadres of "N/A" bij gebruik van e-mail.
+        arbitration_website: Kan een webformulier zijn, of "N/A" wanneer e-mail wordt gebruikt.
+        choice_of_law: Stad, regio, grondgebied of staat waar de interne materiële wetten van toepassing zijn op alle aanspraken.
         dmca_address: 'Gebruik voor beheerders in de VS: het adres dat is geregistreerd in de DMCA Designated Agent Directory. Op verzoek is er een postbuslijst beschikbaar. Gebruik het DMCA Designated Agent Post Office Box Waiver Request om het Copyright Office te e-mailen en te beschrijven dat je een vanaf huis opererende inhoudsmoderator bent, die wraak of vergelding vreest voor je moderator-acties en daarom een postbus moet gebruiken om jouw huisadres uit het publieke domein te houden.'
-        dmca_email: Kan hetzelfde e-mailadres zijn dat gebruikt wordt voor "E-mailadres voor juridische berichten" hierboven
+        dmca_email: Kan hetzelfde e-mailadres zijn dat gebruikt wordt voor "E-mailadres voor juridische berichten" hierboven.
         domain: Een unieke identificatie van de online dienst die je levert.
         jurisdiction: Vermeld het land waar de persoon woont die de rekeningen betaalt. Is het een bedrijf of iets dergelijks, vermeld dan het land waar het ingeschreven staat en de stad, de regio, het grondgebied of de staat, voor zover van toepassing.
+        min_age: Mag niet lager zijn dan de minimale vereiste leeftijd volgens de wetten van jouw jurisdictie.
       user:
         chosen_languages: Alleen berichten in de aangevinkte talen worden op de openbare tijdlijnen getoond
+        date_of_birth: We moeten ervoor zorgen dat je tenminste %{age} bent om Mastodon te gebruiken. Dit wordt niet opgeslagen.
         role: De rol bepaalt welke rechten de gebruiker heeft.
       user_role:
         color: Kleur die gebruikt wordt voor de rol in de UI, als RGB in hexadecimale formaat
@@ -255,6 +261,7 @@ nl:
         name: Hashtag
       filters:
         actions:
+          blur: Media met een waarschuwing verbergen
           hide: Volledig verbergen
           warn: Met een waarschuwing verbergen
       form_admin_settings:
@@ -268,6 +275,7 @@ nl:
         favicon: Favicon
         mascot: Aangepaste mascotte (legacy)
         media_cache_retention_period: Bewaartermijn mediacache
+        min_age: Vereiste minimumleeftijd
         peers_api_enabled: Lijst van bekende servers via de API publiceren
         profile_directory: Gebruikersgids inschakelen
         registrations_mode: Wie kan zich registreren
@@ -333,16 +341,22 @@ nl:
         usable: Berichten toestaan deze hashtag lokaal te gebruiken
       terms_of_service:
         changelog: Wat is veranderd?
+        effective_date: Ingangsdatum
         text: Gebruiksvoorwaarden
       terms_of_service_generator:
         admin_email: E-mailadres voor juridische meldingen
         arbitration_address: Vestigingsadres voor arbitrage-mededelingen
         arbitration_website: Website voor het indienen van arbitrage-mededelingen
+        choice_of_law: Keuze van rechtsgebied
         dmca_address: Vestigingsadres voor DMCA/auteursrecht-mededelingen
         dmca_email: E-mailadres voor DMCA/auteursrecht-mededelingen
         domain: Domein
         jurisdiction: Jurisdictie
+        min_age: Minimumleeftijd
       user:
+        date_of_birth_1i: Dag
+        date_of_birth_2i: Maand
+        date_of_birth_3i: Jaar
         role: Rol
         time_zone: Tijdzone
       user_role:
diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml
index b42fdc86f5..1a33a4b91d 100644
--- a/config/locales/simple_form.nn.yml
+++ b/config/locales/simple_form.nn.yml
@@ -3,6 +3,7 @@ nn:
   simple_form:
     hints:
       account:
+        attribution_domains: Ein per line. Vernar mot falske krediteringar.
         discoverable: Dei offentlege innlegga dine og profilen din kan dukka opp i tilrådingar på ulike stader på Mastodon, og profilen din kan bli føreslegen for andre folk.
         display_name: Ditt fulle namn eller ditt tøysenamn.
         fields: Heimesida di, pronomen, alder, eller kva du måtte ynskje.
@@ -74,6 +75,7 @@ nn:
       filters:
         action: Velg kva som skal gjerast når eit innlegg samsvarar med filteret
         actions:
+          blur: Gøym media bak ei åtvaring utan å gøyme sjølve teksten
           hide: Skjul filtrert innhald fullstendig og lat som om det ikkje finst
           warn: Skjul det filtrerte innhaldet bak ei åtvaring som nemner tittelen på filteret
       form_admin_settings:
@@ -134,12 +136,13 @@ nn:
         text: Du kan bruka Markdown-syntaks for struktur.
       terms_of_service_generator:
         admin_email: Juridiske merknader kan vera motsegner, rettsavgjerder, orskurdar eller førespurnader om sletting.
-        arbitration_address: Kan vera lik den fysiske adressa over, eller "N/A" viss du bruker epost
-        arbitration_website: Kan vera eit nettskjema, eller "N/A" viss du bruker epost
+        arbitration_address: Kan vere lik den fysiske adressa over, eller "N/A" viss du bruker epost.
+        arbitration_website: Kan vere eit nettskjema eller "N/A" viss du bruker e-post.
         dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
-        dmca_email: Kan vera den same epostadressa som adressa for juridiske merknader over
+        dmca_email: Kan vere same e-post som brukast i "E-postadresse for juridiske meldingar" ovanfor.
         domain: Noko som identifiserer den nettenesta du tilbyr.
         jurisdiction: Skriv kva land den som betaler rekningane bur i. Viss det er eit firma eller ein annan organisasjon, skriv du landet der organisasjonen held til, samt adressa som trengst.
+        min_age: Skal ikkje vere under minstealder som krevst av lover i jurisdiksjonen din.
       user:
         chosen_languages: Når merka vil berre tuta på dei valde språka synast på offentlege tidsliner
         role: Rolla kontrollerer kva løyve brukaren har.
@@ -155,6 +158,7 @@ nn:
         url: Kvar hendingar skal sendast
     labels:
       account:
+        attribution_domains: Nettstader som har lov å kreditera deg
         discoverable: Ta med profilen og innlegga i oppdagingsalgoritmar
         fields:
           name: Merkelapp
@@ -253,6 +257,7 @@ nn:
         name: Emneknagg
       filters:
         actions:
+          blur: Gøym media med ei åtvaring
           hide: Gøym heilt
           warn: Gøym med ei åtvaring
       form_admin_settings:
@@ -340,6 +345,7 @@ nn:
         dmca_email: Epostadresse for opphavsrettsvarsel
         domain: Domene
         jurisdiction: Rettskrins
+        min_age: Minstealder
       user:
         role: Rolle
         time_zone: Tidssone
diff --git a/config/locales/simple_form.no.yml b/config/locales/simple_form.no.yml
index fb7a808876..c93c4d40bc 100644
--- a/config/locales/simple_form.no.yml
+++ b/config/locales/simple_form.no.yml
@@ -234,6 +234,7 @@
           warn: Skjul med en advarsel
       form_admin_settings:
         activity_api_enabled: Publiser samlet statistikk om brukeraktivitet i API
+        app_icon: App-ikon
         backups_retention_period: Brukers oppbevaringsperiode for arkiv
         bootstrap_timeline_accounts: Anbefaler alltid disse kontoene til nye brukere
         closed_registrations_message: Egendefinert melding når registrering ikke er tilgjengelig
@@ -301,6 +302,10 @@
         listable: Tillat denne emneknaggen å vises i søk og på profilmappen
         name: Emneknagg
         trendable: Tillat denne emneknaggen til å vises under trender
+      terms_of_service:
+        text: Bruksvilkår
+      terms_of_service_generator:
+        domain: Domene
       user:
         role: Rolle
         time_zone: Tidssone
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index efa17c1a9b..c049822b81 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -7,7 +7,7 @@ pl:
         discoverable: Twój profil i publiczne wpisy mogą być promowane lub polecane na Mastodonie i twój profil może być sugerowany innym użytkownikom.
         display_name: Twoje imię lub pseudonim.
         fields: Co ci się tylko podoba – twoja strona domowa, zaimki, wiek…
-        indexable: Twoje publiczne wpisy mogą pojawiać się w wynikach wyszukiwania w Mastodonie. Użytkownicy, którzy wchodzili w interakcje z twoimi wpisami będą mogli je znaleźć niezależnie od tego ustawienia.
+        indexable: Twoje publiczne wpisy mogą pojawiać się w wynikach wyszukiwania w Mastodonie. Użytkownicy, którzy reagowali na Twoje wpisy, będą mogli je znaleźć niezależnie od tego ustawienia.
         note: 'Możesz @wspomnieć użytkowników albo #hasztagi.'
         show_collections: Twoja lista obserwowanych i obserwujących będzie widoczna dla wszystkich. Użytkownicy których obserwujesz będą jednak o tym wiedzieli.
         unlocked: Inni użytkownicy będą mogli cię obserwować bez proszenia o zgodę. Odznacz, jeżeli chcesz aprobować obserwujących ręcznie po przejrzeniu próśb o obserwowanie.
@@ -135,8 +135,10 @@ pl:
         text: Może być stworzony przy pomocy składni Markdown.
       terms_of_service_generator:
         admin_email: Zawiadomienia prawne obejmują środki zapobiegawcze, nakazy sądowe, wnioski o popełnienie sprawy oraz wnioski organów ścigania.
-        arbitration_address: Może być taki sam jak adres fizyczny powyżej lub „N/A” jeśli używasz adresu e-mail
-        arbitration_website: Może być formularzem internetowym lub „N/A”, jeśli używasz adresu e-mail
+        choice_of_law: Miasto, region, terytorium lub stan, którego wewnętrzne prawo będzie regulowało wszelkie roszczenia.
+        dmca_address: W przypadku operatorów z USA należy użyć adresu zarejestrowanego w DMCA Designated Agent Directory. Lista skrytek pocztowych dostępna jest na bezpośrednią prośbę użytkownika. Użyj DMCA Agent Post Office Box Waiver Request, aby wysłać email do Copyright Office z informacją, że jesteś domowym administratorm treści i z powodu obawy o zemstę lub odwetu za swoje działania, musisz użyć skrytki pocztowej, żeby usunąć swój adres domowy z dostępu publicznego.
+        domain: Unikalny numer identyfikacji świadczonej przez Ciebie usługi online.
+        jurisdiction: Wymień państwo, w którym mieszkają osoby płacące rachunki. Jeżeli jest to spółka lub inny zarejestrowany podmiot, w zależności od przypadku podaj państwo, w którym jest zarejestrowany, a także miasto, region czy województwo.
       user:
         chosen_languages: Jeżeli zaznaczone, tylko wpisy w wybranych językach będą wyświetlane na publicznych osiach czasu
         role: Rola kontroluje uprawnienia użytkownika.
@@ -152,6 +154,7 @@ pl:
         url: Dokąd będą wysłane zdarzenia
     labels:
       account:
+        attribution_domains: Strony które mogą ci przypisywać autorstwo
         discoverable: Udostępniaj profil i wpisy funkcjom odkrywania
         fields:
           name: Nazwa
@@ -228,6 +231,7 @@ pl:
         setting_display_media_show_all: Pokaż wszystko
         setting_expand_spoilers: Zawsze rozwijaj wpisy oznaczone ostrzeżeniem o zawartości
         setting_hide_network: Ukryj swoją sieć
+        setting_missing_alt_text_modal: Pokaż okno potwierdzenia przed opublikowaniem materiałów bez pomocniczego opisu obrazów
         setting_reduce_motion: Ogranicz ruch w animacjach
         setting_system_font_ui: Używaj domyślnej czcionki systemu
         setting_system_scrollbars_ui: Używaj domyślnego paska przewijania systemu
@@ -329,8 +333,10 @@ pl:
         changelog: Co się zmieniło?
         text: Warunki korzystania z usługi
       terms_of_service_generator:
+        admin_email: Adres e-mail przeznaczony do celów prawnych
         arbitration_address: Adres fizyczny powiadomień arbitrażowych
         arbitration_website: Strona internetowa do składania zgłoszeń arbitrażowych
+        choice_of_law: Wybór prawa
         dmca_address: Adres fizyczny dla zgłoszeń naruszenia DMCA/praw autorskich
         dmca_email: Adres e-mail dla zgłoszeń naruszenia DMCA/praw autorskich
         domain: Domena
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index 500fb4c30e..64221777d7 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -3,13 +3,14 @@ pt-BR:
   simple_form:
     hints:
       account:
+        attribution_domains: Um por linha. Protege contra atribuições falsas.
         discoverable: Suas publicações e perfil públicos podem ser destaques ou recomendados em várias áreas de Mastodon, e seu perfil pode ser sugerido a outros usuários.
         display_name: Seu nome completo ou apelido.
         fields: Sua página inicial, pronomes, idade ou qualquer coisa que quiser.
         indexable: Suas publicações públicas podem aparecer nos resultados da pesquisa em Mastodon. As pessoas que interagiram com suas publicações podem conseguir pesquisá-las independentemente disso.
-        note: 'Você pode @mencionar outras pessoas ou #hashtags.'
-        show_collections: As pessoas poderão navegar entre os seus seguidores e seguidores. As pessoas que você segue verão que você as segue independentemente disso.
-        unlocked: As pessoas poderão seguir você sem solicitar aprovação. Desmarque caso você queira revisar as solicitações de seguidor e escolha se queira aceitar ou rejeitar novos seguidores.
+        note: 'Você pode @mencionar outras pessoas ou usar #hashtags.'
+        show_collections: As pessoas podem ver seus seguidores e quem você está seguindo. Os perfis que você seguir saberão que você os segue independentemente do que selecionar.
+        unlocked: As pessoas poderão te seguir sem solicitar aprovação. Desmarque caso você queira revisar as solicitações.
       account_alias:
         acct: Especifique o usuário@domínio de onde veio
       account_migration:
@@ -26,8 +27,8 @@ pt-BR:
           disable: Impede o usuário de usar a conta, porém sem excluí-la ou suspendê-la.
           none: Use isto para enviar uma advertência ao usuário, sem nenhuma outra ação.
           sensitive: Marca todas as mídias do usuário como sensível.
-          silence: Impede o usuário de enviar postagens visualmente públicas, além de ocultar suas publicações e notificações dos que não o seguem. Ademais, fecha todas as denúncias contra esta conta.
-          suspend: Impede qualquer interação de ou para esta conta e exclui seu conteúdo. Reversível dentro de 30 dias. Ademais, fecha todas as denúncias contra esta conta.
+          silence: Impede o usuário de postar publicamente, restringe suas publicações e notificações a somente quem o segue. Encerra todas as denúncias contra esta conta.
+          suspend: Impede qualquer interação de ou para esta conta e exclui seu conteúdo. Reversível dentro de 30 dias. Encerra todas as denúncias contra esta conta.
         warning_preset_id: Opcional. Você pode adicionar texto personalizado no final da advertência pré-definida
       announcement:
         all_day: Quando marcada, apenas as datas do período serão mostradas
@@ -36,11 +37,11 @@ pt-BR:
         starts_at: Opcional. Caso o comunicado esteja vinculado a um período específico
         text: Você pode usar a sintaxe do toot. Considere o espaço que o comunicado ocupará na tela do usuário
       appeal:
-        text: Você só pode solicitar uma revisão uma vez
+        text: Você só pode recorrer uma vez
       defaults:
         autofollow: Pessoas que criarem conta através de seu convite te seguirão automaticamente
         avatar: WEBP, PNG, GIF ou JPG. No máximo %{size}. Será reduzido para %{dimensions}px
-        bot: Essa conta executa principalmente ações automatizadas e pode não ser monitorada
+        bot: Sinaliza aos outros de que essa conta executa principalmente ações automatizadas e pode não ser monitorada
         context: Um ou mais contextos onde o filtro deve atuar
         current_password: Para fins de segurança, digite a senha da conta atual
         current_username: Para confirmar, digite o nome de usuário da conta atual
@@ -53,7 +54,7 @@ pt-BR:
         password: Use pelo menos 8 caracteres
         phrase: Corresponderá independente de maiúsculas ou minúsculas, no texto ou no Aviso de Conteúdo de um toot
         scopes: Quais APIs o aplicativo vai ter permissão de acessar. Se você selecionar uma autorização de alto nível, você não precisa selecionar individualmente os outros.
-        setting_aggregate_reblogs: Não mostra novos impulsos para publicações já receberam recentemente (afeta somente os impulsos mais recentes)
+        setting_aggregate_reblogs: Não mostrar novos impulsos para publicações que já foram impulsionadas recentemente (afeta somente os impulsos mais recentes)
         setting_always_send_emails: Normalmente, as notificações por e-mail não serão enviadas enquanto você estiver usando ativamente o Mastodon
         setting_default_sensitive: Mídia sensível está oculta por padrão e pode ser revelada com um clique
         setting_display_media_default: Sempre ocultar mídia sensível
@@ -74,12 +75,13 @@ pt-BR:
       filters:
         action: Escolher qual ação executar quando uma publicação corresponder ao filtro
         actions:
+          blur: Oculte a mídia com um aviso, porém mantenha o texto visível
           hide: Esconder completamente o conteúdo filtrado, comportando-se como se ele não existisse
           warn: Ocultar o conteúdo filtrado por trás de um aviso mencionando o título do filtro
       form_admin_settings:
         activity_api_enabled: Contagem de publicações locais, usuários ativos e novos usuários semanais
         app_icon: WEBP, PNG, GIF ou JPG. Sobrescrever o ícone padrão do aplicativo em dispositivos móveis com um ícone personalizado.
-        backups_retention_period: Os usuários têm a capacidade de gerar arquivos de suas postagens para baixar mais tarde. Quando definido como um valor positivo, esses arquivos serão automaticamente excluídos do seu armazenamento após o número especificado de dias.
+        backups_retention_period: Os usuários podem gerar arquivos de suas postagens para baixar mais tarde. Quando definido como um valor positivo, esses arquivos serão automaticamente excluídos do seu armazenamento após o número especificado de dias.
         bootstrap_timeline_accounts: Estas contas serão fixadas no topo das recomendações de novos usuários para seguir.
         closed_registrations_message: Exibido quando as inscrições estiverem fechadas
         content_cache_retention_period: Todas as postagens de outros servidores (incluindo boosts e respostas) serão excluídas após o número especificado de dias, sem levar a qualquer interação do usuário local com esses posts. Isto inclui postagens onde um usuário local o marcou como favorito ou favoritos. Menções privadas entre usuários de diferentes instâncias também serão perdidas e impossíveis de restaurar. O uso desta configuração destina-se a instâncias especiais de propósitos e quebra muitas expectativas dos usuários quando implementadas para uso de propósito geral.
@@ -87,6 +89,7 @@ pt-BR:
         favicon: WEBP, PNG, GIF ou JPG. Sobrescreve o favicon padrão do Mastodon com um ícone personalizado.
         mascot: Substitui a ilustração na interface web avançada.
         media_cache_retention_period: Arquivos de mídia de mensagens de usuários remotos são armazenados em cache no seu servidor. Quando definido como valor positivo, a mídia será excluída após o número especificado de dias. Se os dados da mídia forem solicitados depois de excluídos, eles serão baixados novamente, se o conteúdo fonte ainda estiver disponível. Devido a restrições de quantas vezes os cartões de visualização de links sondam sites de terceiros, é recomendado definir este valor em pelo menos 14 dias, ou pré-visualização de links não serão atualizados a pedido antes desse tempo.
+        min_age: Os usuários precisarão confirmar sua data de nascimento no cadastro
         peers_api_enabled: Uma lista de nomes de domínio que este servidor encontrou no "fediverse". Nenhum dado é incluído aqui sobre se você concorda com os padroes operacionais de um determinado servidor, apenas que o seu servidor sabe disso. Esta ferramenta é utilizado por serviços que recolhem estatísticas sob as normas da federação (grupo de empresas que concordam sob paramentros operacionais específicos), em termos gerais.
         profile_directory: O diretório de perfis lista todos os usuários que optaram por permitir que suas contas sejam descobertas.
         require_invite_text: 'Quando o cadastro de novas contas exigir aprovação manual, tornar obrigatório, ao invés de opcional, o texto de solicitação de convite: "Por que você deseja ingressar nessa comunidade?"'
@@ -129,10 +132,23 @@ pt-BR:
         show_application: Você sempre conseguirá ver qual aplicativo realizou sua publicação independentemente disso.
       tag:
         name: Você pode mudar a capitalização das letras, por exemplo, para torná-la mais legível
+      terms_of_service:
+        changelog: Pode ser estruturado com a sintaxe Markdown.
+        effective_date: Um intervalo de tempo razoável pode variar de 10 a 30 dias a partir da data em que notificar seus usuários.
+        text: Pode ser estruturado com a sintaxe Markdown.
       terms_of_service_generator:
-        dmca_email: Pode ser o mesmo e-mail utilizado para "Endereço de e-mail para avisos legais" acima
+        admin_email: Avisos legais incluem contra-notificações, ordens judiciais, solicitações de remoção e solicitações de órgãos de fiscalização.
+        arbitration_address: Pode ser o mesmo do endereço colocado cima, ou use "N/A" se estiver utilizando e-mail.
+        arbitration_website: Pode ser um formulário online, ou use "N/A" caso esteja utilizando um e-mail.
+        choice_of_law: Cidade, região, território ou estado cujas leis substantivas internas serão aplicáveis a todas e quaisquer reivindicações.
+        dmca_address: Para operadores dos EUA, utilize o endereço registrado no Diretório de Agentes Designados pela DMCA. Um endereço de Caixa Postal está disponível mediante solicitação direta; use a Solicitação de Isenção de Caixa Postal de Agente Designado pela DMCA para enviar um e-mail ao Escritório de Direitos Autorais, explicando que você é um moderador de conteúdo que trabalha em casa e teme vingança ou retaliação por suas ações, precisando usar uma Caixa Postal para remover seu endereço residencial da visualização pública.
+        dmca_email: Pode ser o mesmo e-mail utilizado acima em "Endereço de e-mail para avisos legais".
+        domain: Identificação única do serviço online que você está fornecendo.
+        jurisdiction: Liste o país onde quem paga as contas reside. Se for uma empresa ou outra entidade, liste o país onde ela está incorporada, e a cidade, região, território ou estado, conforme apropriado.
+        min_age: Não deve ter menos que a idade mínima exigida pelas suas leis locais.
       user:
         chosen_languages: Apenas as publicações dos idiomas selecionados serão exibidas nas linhas públicas
+        date_of_birth: Precisamos ter certeza de que você tem, no mínimo, %{age} anos para usar o Mastodon. Não armazenaremos essa informação.
         role: A função controla quais permissões o usuário tem.
       user_role:
         color: Cor a ser usada para o cargo em toda a interface do usuário, como RGB no formato hexadecimal
@@ -146,6 +162,7 @@ pt-BR:
         url: Aonde os eventos serão enviados
     labels:
       account:
+        attribution_domains: Sites autorizados a creditar você.
         discoverable: Destacar perfil e publicações nos algoritmos de descoberta
         fields:
           name: Rótulo
@@ -222,6 +239,7 @@ pt-BR:
         setting_display_media_show_all: Mostrar tudo
         setting_expand_spoilers: Sempre expandir toots com Aviso de Conteúdo
         setting_hide_network: Ocultar suas relações
+        setting_missing_alt_text_modal: Mostrar caixa de diálogo de confirmação antes de postar mídia sem texto alternativo.
         setting_reduce_motion: Reduzir animações
         setting_system_font_ui: Usar fonte padrão do sistema
         setting_system_scrollbars_ui: Usar barra de rolagem padrão do sistema
@@ -243,6 +261,7 @@ pt-BR:
         name: Hashtag
       filters:
         actions:
+          blur: Oculte a mídia com um aviso
           hide: Ocultar completamente
           warn: Ocultar com um aviso
       form_admin_settings:
@@ -256,6 +275,7 @@ pt-BR:
         favicon: Favicon
         mascot: Mascote personalizado (legado)
         media_cache_retention_period: Período de retenção do cachê de mídia
+        min_age: Requisito de idade mínimia
         peers_api_enabled: Publicar lista de instâncias de servidor descobertas na API
         profile_directory: Ativar diretório de perfis
         registrations_mode: Quem pode se inscrever
@@ -319,7 +339,24 @@ pt-BR:
         name: Hashtag
         trendable: Permitir que esta hashtag fique em alta
         usable: Permitir que as publicações usem esta hashtag localmente
+      terms_of_service:
+        changelog: O que mudou?
+        effective_date: Data de vigência
+        text: Termos de Serviço
+      terms_of_service_generator:
+        admin_email: Endereço de e-mail para avisos legais
+        arbitration_address: Endereço físico para avisos de arbitragem.
+        arbitration_website: Site para submissão de notificações de arbitragem
+        choice_of_law: Lei de regência
+        dmca_address: Endereço físico para notificações de DMCA/direitos autorais
+        dmca_email: Endereço de e-mail para notificações de DMCA/direitos autorais
+        domain: Domínio
+        jurisdiction: Jurisdição legal
+        min_age: Idade mínima
       user:
+        date_of_birth_1i: Dia
+        date_of_birth_2i: Mês
+        date_of_birth_3i: Ano
         role: Cargo
         time_zone: Fuso horário
       user_role:
diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml
index 8880b3d892..5549ef40d6 100644
--- a/config/locales/simple_form.pt-PT.yml
+++ b/config/locales/simple_form.pt-PT.yml
@@ -75,6 +75,7 @@ pt-PT:
       filters:
         action: Escolha qual a ação a executar quando uma publicação corresponde ao filtro
         actions:
+          blur: Esconder multimédia com um aviso à frente, sem esconder o texto
           hide: Ocultar completamente o conteúdo filtrado, comportando-se como se não existisse
           warn: Ocultar o conteúdo filtrado por trás de um aviso mencionando o título do filtro
       form_admin_settings:
@@ -88,6 +89,7 @@ pt-PT:
         favicon: WEBP, PNG, GIF ou JPG. Substitui o ícone de favorito padrão do Mastodon por um ícone personalizado.
         mascot: Sobrepõe-se à ilustração na interface web avançada.
         media_cache_retention_period: Os ficheiros multimédia de publicações feitas por utilizadores remotos são armazenados em cache no seu servidor. Quando definido para um valor positivo, os ficheiros multimédia serão eliminados após o número de dias especificado. Se os ficheiros multimédia forem solicitados depois de terem sido eliminados, serão transferidos novamente, se o conteúdo de origem ainda estiver disponível. Devido a restrições sobre a frequência com que os cartões de pré-visualização de links pesquisam sites de terceiros, recomenda-se que este valor seja definido para, pelo menos, 14 dias, ou os cartões de pré-visualização de links não serão atualizados a pedido antes desse período.
+        min_age: Os utilizadores serão convidados a confirmar a sua data de nascimento durante o processo de inscrição
         peers_api_enabled: Uma lista de nomes de domínio que este servidor encontrou no fediverso. Nenhum dado é incluído aqui sobre se você federa com um determinado servidor, apenas que o seu servidor o conhece. Este serviço é utilizado por serviços que recolhem estatísticas na federação, em termos gerais.
         profile_directory: O diretório de perfis lista todos os utilizadores que optaram por ter a sua conta a ser sugerida a outros.
         require_invite_text: Quando as incrições exigirem aprovação manual, faça o texto "Por que se quer juntar a nós?" da solicitação de convite ser obrigatório, em vez de opcional
@@ -132,17 +134,21 @@ pt-PT:
         name: Só pode alterar a capitalização das letras, por exemplo, para torná-las mais legíveis
       terms_of_service:
         changelog: Pode ser estruturado com sintaxe Markdown.
+        effective_date: Um período razoável pode ir de 10 a 30 dias desde a data em que notifica os seus utilizadores.
         text: Pode ser estruturado com sintaxe Markdown.
       terms_of_service_generator:
         admin_email: Os avisos legais incluem contra-avisos, ordens judiciais, pedidos de remoção e pedidos de aplicação da lei.
-        arbitration_address: Pode ser o mesmo que o endereço físico acima ou “N/A” se utilizar e-mail
-        arbitration_website: Pode ser um formulário web ou “N/A” se for utilizado o e-mail
+        arbitration_address: Pode ser o mesmo que o endereço físico acima ou “N/A” se utilizar e-mail.
+        arbitration_website: Pode ser um formulário web ou “N/A” se for utilizado o e-mail.
+        choice_of_law: A legislação interna do estado, território, região ou cidade que deve regular qualquer conflito.
         dmca_address: Para operadores dos EUA, utilize o endereço registado no Diretório de Agentes Designados DMCA. A listagem de uma caixa postal está disponível mediante pedido direto. Utilize o DMCA Designated Agent Post Office Box Waiver Request para enviar uma mensagem de correio eletrónico ao Copyright Office e descreva que é um moderador de conteúdos baseado em casa que receia vingança ou represálias pelas suas ações e que necessita de utilizar uma caixa postal para retirar o seu endereço de casa da vista do público.
-        dmca_email: Pode ser o mesmo e-mail utilizado para “Endereço de e-mail para avisos legais” acima
+        dmca_email: Pode ser o mesmo e-mail utilizado para “Endereço de e-mail para avisos legais” acima.
         domain: Identificação única do serviço online que está a prestar.
         jurisdiction: Indique o país de residência de quem paga as contas. Se se tratar de uma empresa ou outra entidade, indique o país onde está constituída, bem como a cidade, região, território ou estado, consoante o caso.
+        min_age: Não deve ter menos do que a idade mínima exigida pela legislação da sua jurisdição.
       user:
         chosen_languages: Quando selecionado, só serão mostradas nas cronologias públicas as publicações nos idiomas escolhidos
+        date_of_birth: Temos de ter a certeza de que tens pelo menos %{age} para usar o Mastodon. Não vamos guardar esta informação.
         role: A função controla as permissões que o utilizador tem.
       user_role:
         color: Cor a ser utilizada para a função em toda a interface de utilizador, como RGB no formato hexadecimal
@@ -233,7 +239,7 @@ pt-PT:
         setting_display_media_show_all: Mostrar todos
         setting_expand_spoilers: Expandir sempre as publicações marcadas com avisos de conteúdo
         setting_hide_network: Esconder a tua rede
-        setting_missing_alt_text_modal: Mostrar janela de confirmação antes de publicar elementos gráficos sem texto alternativo
+        setting_missing_alt_text_modal: Mostrar janela de confirmação antes de publicar multimédia sem texto alternativo
         setting_reduce_motion: Reduzir movimento em animações
         setting_system_font_ui: Usar o tipo de letra padrão do sistema
         setting_system_scrollbars_ui: Utilizar a barra de deslocação predefinida do sistema
@@ -255,6 +261,7 @@ pt-PT:
         name: Etiqueta
       filters:
         actions:
+          blur: Esconder multimédia com um aviso
           hide: Ocultar por completo
           warn: Ocultar com um aviso
       form_admin_settings:
@@ -268,6 +275,7 @@ pt-PT:
         favicon: Ícone de favoritos
         mascot: Mascote personalizada (legado)
         media_cache_retention_period: Período de retenção de ficheiros multimédia em cache
+        min_age: Idade mínima requerida
         peers_api_enabled: Publicar lista de servidores descobertos na API
         profile_directory: Ativar o diretório de perfis
         registrations_mode: Quem se pode inscrever
@@ -333,16 +341,22 @@ pt-PT:
         usable: Permitir que as publicações usem esta etiqueta localmente
       terms_of_service:
         changelog: O que mudou?
+        effective_date: Data de entrada em vigor
         text: Termos do serviço
       terms_of_service_generator:
         admin_email: Endereço de e-mail para avisos legais
         arbitration_address: Endereço físico para avisos de arbitragem
         arbitration_website: Website para enviar avisos de arbitragem
+        choice_of_law: Escolha da legislação
         dmca_address: Endereço físico para avisos DMCA / direitos autorais
         dmca_email: Endereço de e-mail para avisos DMCA/direitos autorais
         domain: Domínio
         jurisdiction: Jurisdição legal
+        min_age: Idade mínima
       user:
+        date_of_birth_1i: Dia
+        date_of_birth_2i: Mês
+        date_of_birth_3i: Ano
         role: Função
         time_zone: Fuso horário
       user_role:
diff --git a/config/locales/simple_form.ro.yml b/config/locales/simple_form.ro.yml
index 978f1e4fd4..5cb4e116a2 100644
--- a/config/locales/simple_form.ro.yml
+++ b/config/locales/simple_form.ro.yml
@@ -139,9 +139,9 @@ ro:
           name: Etichetă
           value: Conținut
       account_alias:
-        acct: Manipularea contului vechi
+        acct: Identificatorul contului vechi
       account_migration:
-        acct: Manipularea contului nou
+        acct: Identificatorul contului nou
       account_warning_preset:
         text: Text presetat
         title: Titlu
diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml
index 64db162ea6..24f24acadb 100644
--- a/config/locales/simple_form.ru.yml
+++ b/config/locales/simple_form.ru.yml
@@ -3,6 +3,7 @@ ru:
   simple_form:
     hints:
       account:
+        attribution_domains: По одному на строку. Защищает от ложных атрибуций.
         discoverable: Ваши публичные сообщения и профиль могут быть показаны или рекомендованы в различных разделах Mastodon, и ваш профиль может быть предложен другим пользователям.
         display_name: Ваше полное имя или псевдоним.
         fields: Ваша домашняя страница, местоимения, возраст - все, что угодно.
@@ -74,6 +75,7 @@ ru:
       filters:
         action: Выберите действие, которое нужно выполнить, когда сообщение соответствует фильтру
         actions:
+          blur: Скрыть медиа с предупреждением, не скрывая сам текст
           hide: Полностью скрыть отфильтрованный контент так, как будто его не существует
           warn: Скрыть отфильтрованный контент за предупреждением с указанием названия фильтра
       form_admin_settings:
@@ -87,6 +89,7 @@ ru:
         favicon: WEBP, PNG, GIF или JPG. Заменяет стандартный фавикон Mastodon на собственный значок.
         mascot: Заменяет иллюстрацию в расширенном веб-интерфейсе.
         media_cache_retention_period: Медиафайлы из сообщений, сделанных удаленными пользователями, кэшируются на вашем сервере. При положительном значении медиафайлы будут удалены через указанное количество дней. Если медиаданные будут запрошены после удаления, они будут загружены повторно, если исходный контент все еще доступен. В связи с ограничениями на частоту опроса карточек предварительного просмотра ссылок на сторонних сайтах рекомендуется устанавливать значение не менее 14 дней, иначе карточки предварительного просмотра ссылок не будут обновляться по запросу до этого времени.
+        min_age: Пользователям при регистрации будет предложено ввести свою дату рождения
         peers_api_enabled: Список доменных имен, с которыми сервер столкнулся в fediverse. Здесь нет данных о том, федерировались ли вы с данным сервером, только что ваш сервер знает об этом. Это используется службами, которые собирают статистику по федерации в общем смысле.
         profile_directory: В каталоге профилей перечислены все пользователи, которые согласились быть доступными для обнаружения.
         require_invite_text: Когда регистрация требует ручного одобрения, сделайте текстовый ввод "Почему вы хотите присоединиться?" обязательным, а не опциональным
@@ -131,17 +134,21 @@ ru:
         name: Вы можете изменить только регистр букв чтобы, например, сделать тег более читаемым
       terms_of_service:
         changelog: Можно использовать синтаксис языка разметки Markdown.
+        effective_date: Разумные временные рамки могут варьироваться в диапазоне от 10 до 30 дней после уведомления пользователей.
         text: Можно использовать синтаксис языка разметки Markdown.
       terms_of_service_generator:
         admin_email: Юридические уведомления включают в себя встречные уведомления, постановления суда, запросы на удаление и запросы правоохранительных органов.
-        arbitration_address: Может совпадать с почтовым адресом, указанным выше, либо «N/A» в случае электронной почты
-        arbitration_website: Веб-форма или «N/A» в случае электронной почты
+        arbitration_address: Может совпадать с почтовым адресом, указанным выше, либо «N/A» в случае электронной почты.
+        arbitration_website: Веб-форма или «N/A» в случае электронной почты.
+        choice_of_law: Город, регион, территория или государство, внутреннее материальное право которого регулирует любые претензии.
         dmca_address: Находящиеся в США операторы должны использовать адрес, зарегистрированный в DMCA Designated Agent Directory. Использовать абонентский ящик возможно при обращении в соответствующей просьбой, для чего нужно с помощью DMCA Designated Agent Post Office Box Waiver Request написать сообщение в Copyright Office и объяснить, что вы занимаетесь модерацией контента из дома и опасаетесь мести за свои действия, поэтому должны использовать абонентский ящик, чтобы убрать ваш домашний адрес из общего доступа.
-        dmca_email: Может совпадать с адресом электронной почты для юридических уведомлений, указанным выше
+        dmca_email: Может совпадать с адресом электронной почты для юридических уведомлений, указанным выше.
         domain: Имя, позволяющее уникально идентифицировать ваш онлайн-ресурс.
         jurisdiction: Впишите страну, где находится лицо, оплачивающее счета. Если это компания либо организация, впишите страну инкорпорации, включая город, регион, территорию или штат, если это необходимо.
+        min_age: Не меньше минимального возраста, требуемого по закону в вашей юрисдикции.
       user:
         chosen_languages: Если выбрано, то в публичных лентах будут показаны только посты на выбранных языках.
+        date_of_birth: Нужно убедиться, что вам не меньше %{age} лет. Мы не храним введённые здесь данные.
         role: Роль определяет, какими правами обладает пользователь.
       user_role:
         color: Цвет, который будет использоваться для роли в интерфейсе (UI), как RGB в формате HEX
@@ -155,6 +162,7 @@ ru:
         url: Куда события будут отправляться
     labels:
       account:
+        attribution_domains: Веб-сайты, которым разрешено ссылаться на вас
         discoverable: Профиль и сообщения в алгоритмах обнаружения
         fields:
           name: Пункт
@@ -231,6 +239,7 @@ ru:
         setting_display_media_show_all: Показывать все
         setting_expand_spoilers: Всегда раскрывать посты, имеющие предупреждение о содержании
         setting_hide_network: Скрыть свои связи
+        setting_missing_alt_text_modal: Всегда спрашивать перед публикацией медиафайлов без альтернативного текста
         setting_reduce_motion: Уменьшить движение в анимации
         setting_system_font_ui: Использовать шрифт системы по умолчанию
         setting_system_scrollbars_ui: Использовать системные полосы прокрутки
@@ -252,6 +261,7 @@ ru:
         name: Добавить хэштег
       filters:
         actions:
+          blur: Скрыть медиа с предупреждением
           hide: Скрыть полностью
           warn: Скрыть с предупреждением
       form_admin_settings:
@@ -265,6 +275,7 @@ ru:
         favicon: Favicon
         mascot: Пользовательский маскот (устаревшее)
         media_cache_retention_period: Период хранения кэша медиафайлов
+        min_age: Требование минимального возраста
         peers_api_enabled: Публикация списка обнаруженных узлов в API
         profile_directory: Включить каталог профилей
         registrations_mode: Кто может зарегистрироваться
@@ -330,16 +341,22 @@ ru:
         usable: Позволить этот хэштег в локальных сообщениях
       terms_of_service:
         changelog: Что изменилось?
+        effective_date: Дата вступления в силу
         text: Пользовательское соглашение
       terms_of_service_generator:
         admin_email: Адрес электронной почты для юридических уведомлений
         arbitration_address: Почтовый адрес для уведомлений об арбитраже
         arbitration_website: Вебсайт для подачи уведомления об арбитраже
+        choice_of_law: Юрисдикция
         dmca_address: Почтовый адрес для обращений правообладателей
         dmca_email: Адрес электронной почты для обращений правообладателей
         domain: Доменное имя
         jurisdiction: Юрисдикция
+        min_age: Минимальный возраст
       user:
+        date_of_birth_1i: День
+        date_of_birth_2i: Месяц
+        date_of_birth_3i: Год
         role: Роль
         time_zone: Часовой пояс
       user_role:
diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml
index 1f1867854e..5d55844aa9 100644
--- a/config/locales/simple_form.sl.yml
+++ b/config/locales/simple_form.sl.yml
@@ -3,12 +3,14 @@ sl:
   simple_form:
     hints:
       account:
+        attribution_domains: Ena na vrstico. Ščiti pred napačno navedbo avtorstva.
         discoverable: Vaše javne objave in profil so lahko predstavljeni ali priporočeni v različnih delih Mastodona, vaš profil pa je lahko predlagan drugim uporabnikom.
         display_name: Vaše polno ime ali lažno ime.
         fields: Vaša domača stran, starost, kar koli.
         indexable: Vaše javne objave se lahko pojavijo v rezultatih iskanja na Mastodonu. Ljudje, ki so bili v interakciji z vašimi objavami, jih bodo lahko iskali ne glede na to.
         note: 'Druge osebe lahko @omenite ali #ključnite.'
         show_collections: Ljudje bodo lahko brskali po vaših sledilcih in sledenih. Ljudje, ki jim sledite, bodo videli, da jim sledite ne glede na to.
+        unlocked: Ljudje vam bodo lahko sledili, ne da bi zahtevali odobritev. Ne potrdite, če želite pregledati prošnje za sledenje in izbrati, ali želite nove sledilce sprejeti ali zavrniti.
       account_alias:
         acct: Določite uporabniškoime@domena računa, od katerega se želite preseliti
       account_migration:
@@ -58,6 +60,7 @@ sl:
         setting_display_media_default: Skrij medij, ki je označen kot občutljiv
         setting_display_media_hide_all: Vedno skrij vse medije
         setting_display_media_show_all: Vedno pokaži medij, ki je označen kot občutljiv
+        setting_system_scrollbars_ui: Velja zgolj za namizne brskalnike, ki temeljijo na Safariju in Chromeu
         setting_use_blurhash: Prelivi temeljijo na barvah skrite vizualne slike, vendar zakrivajo vse podrobnosti
         setting_use_pending_items: Skrij posodobitev časovnice za klikom namesto samodejnega posodabljanja
         username: Uporabite lahko črke, števke in podčrtaje.
@@ -85,6 +88,7 @@ sl:
         favicon: WEBP, PNG, GIF ali JPG. Zamenja privzeto ikono spletne strani Mastodon z ikono po meri.
         mascot: Preglasi ilustracijo v naprednem spletnem vmesniku.
         media_cache_retention_period: Predstavnostne datoteke iz objav uporabnikov na ostalih strežnikih se začasno hranijo na tem strežniku. Ko je nastavljeno na pozitivno vrednost, bodo predstavnostne datoteke izbrisane po nastavljenem številu dni. Če bo predstavnostna datoteka zahtevana po izbrisu, bo ponovno prenešena, če bo vir še vedno na voljo. Zaradi omejitev pogostosti prejemanja predogledov povezav z drugih strani je priporočljivo to vrednost nastaviti na vsaj 14 dni. V nasprotnem predogledi povezav pred tem časom ne bodo osveženi na zahtevo.
+        min_age: Med registracijo bodo morali uporabniki potrditi svoj datum rojstva
         peers_api_enabled: Seznam imen domen, na katere je ta strežnik naletel v fediverzumu. Sem niso vključeni podatki o tem, ali ste v federaciji z danim strežnikom, zgolj to, ali vaš strežnik ve zanj. To uporabljajo storitve, ki zbirajo statistične podatke o federaciji v splošnem smislu.
         profile_directory: Imenik profilov izpiše vse uporabnike, ki so dovolili, da so v njem navedeni.
         require_invite_text: Če registracije zahtevajo ročno potrditev, nastavite vnos besedila pod »Zakaj se želite pridružiti?« za obveznega.
@@ -127,8 +131,23 @@ sl:
         show_application: Ne glede na to boste vedno lahko pogledali, katera aplikacija je objavila vašo objavo.
       tag:
         name: Spremenite lahko le npr. velikost črk (velike/male), da je bolj berljivo
+      terms_of_service:
+        changelog: Uporabite lahko oblikovanje z Markdown.
+        effective_date: Razumen čas do uveljavitve je navadno nekje med 10 in 30 dni od datuma, ko ste obvestili svoje uporabnike.
+        text: Uporabite lahko oblikovanje z Markdown.
+      terms_of_service_generator:
+        admin_email: Pravna obvestila vključujejo odgovore na obvestila, sodne naloge, zahteve za odstranitev in zahteve organov pregona.
+        arbitration_address: Lahko je enak kot fizični naslov zgoraj ali „N/A“, če uporabljate e-pošto.
+        arbitration_website: Lahko je spletni obrazec ali „N/A“, če uporabljate e-pošto.
+        choice_of_law: Mesto, regija, teritorij ali zvezna država, katere področni zakoni urejajo vse tožbene zahtevke.
+        dmca_email: Lahko je enak e-poštni naslov kot v polju „E-poštni naslov za pravna obvestila“ zgoraj.
+        domain: Edinstvena identifikacija spletne storitve, ki jo ponujate.
+        jurisdiction: Navedite državo, kjer živi tisti, ki plačuje račune. Če je to podjetje ali druga entiteta, navedite državo, kjer je bila ustanovljena. Po potrebi dopišite tudi mesto, regijo, teritorij ali zvezno državo.
+        min_age: Ne smete biti mlajši od starostne omejitve, ki jo postavljajo zakoni vašega pravosodnega sistema.
       user:
         chosen_languages: Ko je označeno, bodo v javnih časovnicah prikazane samo objave v izbranih jezikih
+        date_of_birth: Prepričati se moramo, da so uporabniki Mastodona stari vsaj %{age} let. Tega podatka ne bomo shranili.
+        role: Vloga določa, katera dovoljenja ima uporabnik.
       user_role:
         color: Barva, uporabljena za vlogo po celem up. vmesniku, podana v šestnajstiškem zapisu RGB
         highlighted: S tem je vloga javno vidna
@@ -141,6 +160,7 @@ sl:
         url: Kam bodo poslani dogodki
     labels:
       account:
+        attribution_domains: Spletna mesta, ki vas smejo navajati kot avtorja/ico
         discoverable: Izpostavljaj profile in objave v algoritmih odkrivanja
         fields:
           name: Oznaka
@@ -217,8 +237,10 @@ sl:
         setting_display_media_show_all: Prikaži vse
         setting_expand_spoilers: Vedno razširi objave, označene z opozorili o vsebini
         setting_hide_network: Skrij svoje omrežje
+        setting_missing_alt_text_modal: Pred objavo predstavnosti brez nadomestnega besedila pokaži potrditveno okno
         setting_reduce_motion: Zmanjšanje premikanja v animacijah
         setting_system_font_ui: Uporabi privzeto pisavo sistema
+        setting_system_scrollbars_ui: Uporabi privzeti drsni trak sistema
         setting_theme: Tema strani
         setting_trends: Pokaži današnje trende
         setting_unfollow_modal: Pokaži potrditveno okno, preden nekoga prenehamo slediti
@@ -250,6 +272,7 @@ sl:
         favicon: Ikona spletne strani
         mascot: Maskota po meri (opuščeno)
         media_cache_retention_period: Obdobje hrambe predpomnilnika predstavnosti
+        min_age: Spodnja starostna meja
         peers_api_enabled: Objavi seznam odkritih strežnikov v API-ju
         profile_directory: Omogoči imenik profilov
         registrations_mode: Kdo se lahko registrira
@@ -313,7 +336,24 @@ sl:
         name: Ključnik
         trendable: Dovoli, da se ta ključnik pojavi med trendi
         usable: Dovoli, da objave krajevno uporabljajo ta ključnik
+      terms_of_service:
+        changelog: Kaj je novega?
+        effective_date: Datum začetka veljavnosti
+        text: Pogoji uporabe
+      terms_of_service_generator:
+        admin_email: E-poštni naslov za pravna obvestila
+        arbitration_address: Fizični naslov za arbitražna obvestila
+        arbitration_website: Spletišče za vložitev arbitražnih obvestil
+        choice_of_law: Izbira prava
+        dmca_address: Fizični naslov za obvestila DMCA ali o avtorskih pravicah
+        dmca_email: E-poštni naslov za obvestila DMCA ali o avtorskih pravicah
+        domain: Domena
+        jurisdiction: Pravna pristojnost
+        min_age: Najmanjša starost
       user:
+        date_of_birth_1i: Dan
+        date_of_birth_2i: Mesec
+        date_of_birth_3i: Leto
         role: Vloga
         time_zone: Časovni pas
       user_role:
diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml
index fb5e510e70..9f5ada184c 100644
--- a/config/locales/simple_form.sq.yml
+++ b/config/locales/simple_form.sq.yml
@@ -75,6 +75,7 @@ sq:
       filters:
         action: Zgjidhni cili veprim të kryhet, kur një postim ka përputhje me një filtër
         actions:
+          blur: Fshihe median pas një sinjalizimi, pa fshehur vetë tekstin
           hide: Fshihe plotësisht lëndën e filtruar, duke u sjellë sikur të mos ekzistonte
           warn: Fshihe lëndën e filtruar pas një sinjalizimi që përmend titullin e filtrit
       form_admin_settings:
@@ -88,6 +89,7 @@ sq:
         favicon: WEBP, PNG, GIF, ose JPG. Anashkalon favikonën parazgjedhje Mastodon me një ikonë vetjake.
         mascot: Anashkalon ilustrimin te ndërfaqja web e thelluar.
         media_cache_retention_period: Kartela media nga postime të bëra nga përdorues të largët ruhen në një fshehtinë në shërbyesin tuaj. Kur i jepet një vlerë pozitive, media do të fshihet pas numrit të dhënë të ditëve. Nëse të dhënat e medias duhen pas fshirjes, do të rishkarkohen, nëse lënda burim mund të kihet ende. Për shkak kufizimesh mbi sa shpesh skeda paraparjesh lidhjesh ndërveprojnë me sajte palësh të treta, rekomandohet të vihet kjo vlerë të paktën 14 ditë, ose skedat e paraparjes së lidhje s’do të përditësohen duke e kërkuar para asaj kohe.
+        min_age: Përdoruesve do t’ju kërkohet gjatë regjistrimit të ripohojnë datën e lindjes
         peers_api_enabled: Një listë emrash përkatësish që ky shërbyes ka hasur në fedivers. Këtu s’jepen të dhëna nëse jeni i federuar me shërbyesin e dhënë, thjesht tregohet se shërbyesi juaj e njeh. Kjo përdoret nga shërbime që mbledhin statistika mbi federimin në kuptimin e përgjithshëm.
         profile_directory: Drejtoria e profileve paraqet krejt përdoruesit që kanë zgjedhur të jenë të zbulueshëm.
         require_invite_text: Kur regjistrimet lypin miratim dorazi, bëje tekstin “Përse doni të bëheni pjesë?” të detyrueshëm, në vend se opsional
@@ -132,16 +134,20 @@ sq:
         name: Mund të ndryshoni shkronjat vetëm nga të mëdha në të vogla ose anasjelltas, për shembull, për t’i bërë më të lexueshme
       terms_of_service:
         changelog: Mund të strukturohet me sintaksë Markdown.
+        effective_date: Një kuadër kohor i arsyeshëm mund të shtrihet nga 10 deri në 30 ditë nga data që njoftoni përdoruesit tuaj.
         text: Mund të strukturohet me sintaksë Markdown.
       terms_of_service_generator:
         admin_email: Njoftimet ligjore përfshijnë kundërnjoftime, vendime gjyqi, kërkesa për për nxjerrje sajti jashtë shërbimit dhe kërkesa nga organe ligjore.
-        arbitration_address: Mund të jetë e njëjtë me adresën Fizike më sipër, ose “N/A”, nëse përdoret email
-        arbitration_website: Mund të jetë një formular web, ose “N/A”, nëse përdoret email
-        dmca_email: Mund të jetë i njëjti email i përdorur për “Adresë email për njoftime ligjore” më sipëer
+        arbitration_address: Mund të jetë e njëjtë me adresën Fizike më sipër, ose “N/A”, nëse përdoret email.
+        arbitration_website: Mund të jetë një formular web, ose “N/A”, nëse përdoret email.
+        choice_of_law: Qytet, rajon, territor ose shtet, ligjet e brendshme të të cilit do të administrojnë çfarëdo dhe tërë pretendimet.
+        dmca_email: Mund të jetë i njëjti email i përdorur për “Adresë email për njoftime ligjore” më sipër.
         domain: Identifikues unik për shërbimin internetor që po ofroni.
         jurisdiction: Vendosni vendin ku jeton cilido që paguan faturat. Nëse është një shoqëri apo tjetër njësi, vendosni vendin ku është regjistruar, si dhe qytetin, rajonin, territorin apo shtetin përkatës.
+        min_age: S’duhet të jetë nën moshën minimum të domosdoshme nga ligjet në juridiksionin tuaj.
       user:
         chosen_languages: Në iu vëntë shenjë, te rrjedha kohore publike do të shfaqen vetëm mesazhe në gjuhët e përzgjedhura
+        date_of_birth: Na duhet të sigurohemi se jeni të paktën %{age}, që të përdorni Mastodon-in. Këtë s’e depozitojmë.
         role: Roli kontrollon cilat leje ka përdoruesi.
       user_role:
         color: Ngjyrë për t’u përdorur për rolin nëpër UI, si RGB në format gjashtëmbëdhjetësh
@@ -254,6 +260,7 @@ sq:
         name: Hashtag
       filters:
         actions:
+          blur: Fshihe median me një sinjalizim
           hide: Fshihe plotësisht
           warn: Fshihe me një sinjalizim
       form_admin_settings:
@@ -267,6 +274,7 @@ sq:
         favicon: Favikonë
         mascot: Simbol vetjak (e dikurshme)
         media_cache_retention_period: Periudhë mbajtjeje lënde media
+        min_age: Domosdosmëri moshe minimum
         peers_api_enabled: Publiko te API listë shërbyesish të zbuluar
         profile_directory: Aktivizo drejtori profilesh
         registrations_mode: Kush mund të regjistrohet
@@ -332,16 +340,22 @@ sq:
         usable: Lejoji postimet të përdorin lokalisht këtë hashtag
       terms_of_service:
         changelog: Ç’ka ndryshuar?
+        effective_date: Datë efektive
         text: Kushte Shërbimi
       terms_of_service_generator:
         admin_email: Adresë email për njoftime ligjore
         arbitration_address: Adresë fizike për njoftime arbitrazhi
         arbitration_website: Sajtin për parashtrim njoftime arbitrazhi
+        choice_of_law: Zgjedhje Ligji
         dmca_address: Adresë fizike për njoftime DMCA/të drejtash kopjimi
         dmca_email: Adresë email për njoftime DMCA/të drejtash kopjimi
         domain: Përkatësi
         jurisdiction: Juridiksion ligjor
+        min_age: Mosha minimale
       user:
+        date_of_birth_1i: Ditë
+        date_of_birth_2i: Muaj
+        date_of_birth_3i: Vit
         role: Rol
         time_zone: Zonë kohore
       user_role:
diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml
index e35eba36c9..41ac513f39 100644
--- a/config/locales/simple_form.sv.yml
+++ b/config/locales/simple_form.sv.yml
@@ -3,6 +3,7 @@ sv:
   simple_form:
     hints:
       account:
+        attribution_domains: En per rad. Skyddar mot falska attributioner.
         discoverable: Dina offentliga inlägg och din profil kan komma att presenteras eller rekommenderas inom olika områden av Mastodon och din profil kan komma att föreslås till andra användare.
         display_name: Ditt fullständiga namn eller ditt roliga namn.
         fields: Din hemsida, ditt pronomen, din ålder, vadhelst du vill.
@@ -133,9 +134,6 @@ sv:
         changelog: Kan struktureras med Markdown syntax.
         text: Kan struktureras med Markdown syntax.
       terms_of_service_generator:
-        arbitration_address: Kan vara samma som fysisk adress ovan, eller “N/A” om du använder e-post
-        arbitration_website: Kan vara ett webbformulär, eller ”N/A” om du använder e-post
-        dmca_email: Kan vara samma e-postadress som används för “E-postadress för juridiska meddelanden” ovan
         jurisdiction: Lista det land där vem som än betalar räkningarna bor. Om det är ett företag eller annan enhet, lista landet där det är inkorporerat, och staden, regionen, territoriet eller staten på lämpligt sätt.
       user:
         chosen_languages: Vid aktivering visas bara inlägg på dina valda språk i offentliga tidslinjer
@@ -333,7 +331,11 @@ sv:
         dmca_address: Fysisk adress för meddelanden om DMCA/upphovsrätt
         dmca_email: Fysisk adress för meddelanden om DMCA/upphovsrätt
         domain: Domän
+        min_age: Minimiålder
       user:
+        date_of_birth_1i: Dag
+        date_of_birth_2i: Månad
+        date_of_birth_3i: År
         role: Roll
         time_zone: Tidszon
       user_role:
diff --git a/config/locales/simple_form.th.yml b/config/locales/simple_form.th.yml
index db9518759a..530bd60ad1 100644
--- a/config/locales/simple_form.th.yml
+++ b/config/locales/simple_form.th.yml
@@ -3,6 +3,7 @@ th:
   simple_form:
     hints:
       account:
+        attribution_domains: หนึ่งรายการต่อบรรทัด ปกป้องจากการระบุแหล่งที่มาที่ผิด
         discoverable: อาจแสดงหรือแนะนำโพสต์และโปรไฟล์สาธารณะของคุณในพื้นที่ต่าง ๆ ของ Mastodon และอาจเสนอแนะโปรไฟล์ของคุณให้กับผู้ใช้อื่น ๆ
         display_name: ชื่อเต็มของคุณหรือชื่อแบบสนุกสนานของคุณ
         fields: หน้าแรก, สรรพนาม, อายุของคุณ สิ่งใดก็ตามที่คุณต้องการ
@@ -143,6 +144,7 @@ th:
         url: ที่ซึ่งจะส่งเหตุการณ์ไปยัง
     labels:
       account:
+        attribution_domains: เว็บไซต์ที่ได้รับอนุญาตให้ให้เครดิตคุณ
         discoverable: แสดงโปรไฟล์และโพสต์ในอัลกอริทึมการค้นพบ
         fields:
           name: ป้ายชื่อ
@@ -219,6 +221,7 @@ th:
         setting_display_media_show_all: แสดงทั้งหมด
         setting_expand_spoilers: ขยายโพสต์ที่มีการทำเครื่องหมายด้วยคำเตือนเนื้อหาเสมอ
         setting_hide_network: ซ่อนกราฟทางสังคมของคุณ
+        setting_missing_alt_text_modal: แสดงกล่องโต้ตอบการยืนยันก่อนที่จะโพสต์สื่อโดยไม่มีข้อความแสดงแทน
         setting_reduce_motion: ลดการเคลื่อนไหวในภาพเคลื่อนไหว
         setting_system_font_ui: ใช้แบบอักษรเริ่มต้นของระบบ
         setting_system_scrollbars_ui: ใช้แถบเลื่อนเริ่มต้นของระบบ
@@ -321,6 +324,9 @@ th:
       terms_of_service_generator:
         domain: โดเมน
       user:
+        date_of_birth_1i: วัน
+        date_of_birth_2i: เดือน
+        date_of_birth_3i: ปี
         role: บทบาท
         time_zone: โซนเวลา
       user_role:
diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml
index a3be7ae5e3..22300ccec3 100644
--- a/config/locales/simple_form.tr.yml
+++ b/config/locales/simple_form.tr.yml
@@ -75,6 +75,7 @@ tr:
       filters:
         action: Bir gönderi filtreyle eşleştiğinde hangi eylemin yapılacağını seçin
         actions:
+          blur: Medyayı, metnin kendisini gizlemeden bir uyarı arkasında gizle
           hide: Filtrelenmiş içeriği tamamen gizle, sanki varolmamış gibi
           warn: Süzgeçlenmiş içeriği, süzgecinin başlığından söz eden bir uyarının arkasında gizle
       form_admin_settings:
@@ -88,6 +89,7 @@ tr:
         favicon: WEBP, PNG, GIF veya JPG. Varsayılan Mastodon simgesini isteğe bağlı bir simgeyle değiştirir.
         mascot: Gelişmiş web arayüzündeki illüstrasyonu geçersiz kılar.
         media_cache_retention_period: Uzak kullanıcıların gönderilerindeki ortam dosyaları sunucunuzda önbelleklenir. Pozitif bir değer verildiğinde, ortam dosyaları belirlenen gün sonunda silinecektir. Eğer ortam dosyaları silindikten sonra istenirse, kaynak içerik hala mevcutsa, tekrar indirilecektir. Bağlantı önizleme kartlarının üçüncü parti siteleri yoklamasına ilişkin kısıtlamalar nedeniyle, bu değeri en azından 14 gün olarak ayarlamanız önerilir, yoksa bağlantı önizleme kartları bu süreden önce isteğe bağlı olarak güncellenmeyecektir.
+        min_age: Kullanıcılardan kayıt olurken doğum tarihlerini doğrulamaları istenecektir
         peers_api_enabled: Bu sunucunun fediverse'te karşılaştığı alan adlarının bir listesi. İlgili sunucuyla birleştirme mi yapıyorsunuz yoksa sunucunuz sadece onu biliyor mu hakkında bir bilgi burada yok. Bu blgi genel olarak federasyın hakkında istatistik toplamak isteyen hizmetler tarafından kullanılıyor.
         profile_directory: Profil dizini keşfedilebilir olmayı kabul eden tüm kullanıcıları listeler.
         require_invite_text: Kayıt olmak elle doğrulama gerektiriyorsa, "Neden katılmak istiyorsunuz?" metin girdisini isteğe bağlı yerine zorunlu yapın
@@ -132,17 +134,21 @@ tr:
         name: Harflerin, örneğin daha okunabilir yapmak için, sadece büyük/küçük harf durumlarını değiştirebilirsiniz
       terms_of_service:
         changelog: Markdown sözdizimiyle yapılandırılabilir.
+        effective_date: Kullanıcıları bilgilendirmek için mantıklı bir tarih aralığı 10 ile 30 gün arasında olabilir.
         text: Markdown sözdizimiyle yapılandırılabilir.
       terms_of_service_generator:
         admin_email: Yasal bildirimler arasında karşı bildirimler, mahkeme kararları, yayından kaldırma talepleri ve kolluk kuvvetleri talepleri yer alır.
-        arbitration_address: Yukarıdaki fiziksel adresle aynı olabilir, veya e-posta kullanıyorsanız "N/A"
-        arbitration_website: Web formu olabilir, veya e-posta kullanıyorsanız "N/A"
+        arbitration_address: Yukarıdaki fiziksel adresle aynı olabilir, veya e-posta kullanıyorsanız "N/A".
+        arbitration_website: Web formu olabilir, veya e-posta kullanıyorsanız "N/A".
+        choice_of_law: Her türlü iddiayı yönetecek olan iç maddi yasalara ilişkin şehir, bölge, eyalet veya ülke.
         dmca_address: ABD operatörleri için DMCA Atanmış Temsilci Dizininde kayıtlı adresi kullanın. Doğrudan taleple Posta Kutusu kullanımı mümkündür, Telif Hakkı Ofisine e-posta göndermek için DMCA Atanmış Temsilci Posta Kutusu Feragat Talebini kullanın ve eylemleriniz nedeniyle intikam veya cezalandırılmaktan çekinen ve herkese açık ev adresini kullanmamak için Posta Kutusu kullanması gereken ev tabanlı bir içerik moderatörü olduğunuzu açıklayın.
-        dmca_email: Yukarıdaki "Yasal bildirimler için e-posta adresi" için kullanılan e-posta ile aynı olabilir
+        dmca_email: Yukarıdaki "Yasal bildirimler için e-posta adresi" için kullanılan e-posta ile aynı olabilir.
         domain: Sağladığınız çevrimiçi hizmetin benzersiz tanımlaması.
         jurisdiction: Faturaları ödeyen kişinin yaşadığı ülkeyi listeleyin. Bir şirket veya başka bir kuruluş ise, kurulduğu ülkeyi ve uygun şekilde şehri, bölgeyi, yöreyi veya eyaleti listeleyin.
+        min_age: Tabi olduğunuz yasaların gerektirdiği yaştan düşük olmamalıdır.
       user:
         chosen_languages: İşaretlendiğinde, yalnızca seçilen dillerdeki gönderiler genel zaman çizelgelerinde görüntülenir
+        date_of_birth: Mastodon kullanmak için en az %{age} yaşında olduğunuzdan emin olmalıyız. Bu bilgiyi saklamıyoruz.
         role: Rol, kullanıcıların sahip olduğu izinleri denetler.
       user_role:
         color: Arayüz boyunca rol için kullanılacak olan renk, hex biçiminde RGB
@@ -255,6 +261,7 @@ tr:
         name: Etiket
       filters:
         actions:
+          blur: Medyayı bir uyarıyla gizle
           hide: Tamamen gizle
           warn: Uyarıyla gizle
       form_admin_settings:
@@ -268,6 +275,7 @@ tr:
         favicon: Yer imi simgesi
         mascot: Özel maskot (eski)
         media_cache_retention_period: Medya önbelleği saklama süresi
+        min_age: Azami yaş gereksinimi
         peers_api_enabled: API'de keşfedilen sunucuların listesini yayınla
         profile_directory: Profil dizinini etkinleştir
         registrations_mode: Kim kaydolabilir
@@ -333,16 +341,22 @@ tr:
         usable: Gönderilerin yerelde bu etiketi kullanmasına izin ver
       terms_of_service:
         changelog: Ne Değişti?
+        effective_date: Yürürlük tarihi
         text: Hizmet Şartları
       terms_of_service_generator:
         admin_email: Yasal bildirimler için e-posta adresi
         arbitration_address: Tahkim bildirimleri için fiziksel adres
         arbitration_website: Tahkim bildirimlerini göndermek için website
+        choice_of_law: Yasa Tercihi
         dmca_address: DMCA/telif hakkı bildirimleri için fiziksel adres
         dmca_email: DMCA/telif hakkı bildirimleri için e-posta adresi
         domain: Alan adı
         jurisdiction: Yasal yetki alanı
+        min_age: Minimum yaş
       user:
+        date_of_birth_1i: Gün
+        date_of_birth_2i: Ay
+        date_of_birth_3i: Yıl
         role: Rol
         time_zone: Zaman dilimi
       user_role:
diff --git a/config/locales/simple_form.uk.yml b/config/locales/simple_form.uk.yml
index 21a3ccce8e..b43aaeb234 100644
--- a/config/locales/simple_form.uk.yml
+++ b/config/locales/simple_form.uk.yml
@@ -75,6 +75,7 @@ uk:
       filters:
         action: Виберіть дію для виконання коли допис збігається з фільтром
         actions:
+          blur: Приховати медіа за попередженням, не приховуючи сам текст
           hide: Повністю сховати фільтрований вміст, ніби його не існує
           warn: Сховати відфільтрований вміст за попередженням, у якому вказано заголовок фільтра
       form_admin_settings:
@@ -88,6 +89,7 @@ uk:
         favicon: WEBP, PNG, GIF або JPG. Замінює стандартну піктограму Mastodon на власну.
         mascot: Змінює ілюстрацію в розширеному вебінтерфейсі.
         media_cache_retention_period: Медіафайли з дописів віддалених користувачів кешуються на вашому сервері. Якщо встановлено додатне значення, медіа буде видалено через вказану кількість днів. Якщо медіадані будуть запитані після видалення, вони будуть завантажені повторно, якщо вихідний вміст все ще доступний. Через обмеження на частоту опитування карток попереднього перегляду посилань на сторонніх сайтах, рекомендується встановити це значення не менше 14 днів, інакше картки попереднього перегляду посилань не будуть оновлюватися на вимогу раніше цього часу.
+        min_age: Користувачам буде запропоновано підтвердити дату народження під час реєстрації
         peers_api_enabled: Список доменів імен цього сервера з'явився у федівсесвіті. Сюди не входять дані чи ви пов'язані федерацією з цим сервером, а лише відомості, що вашому серверу відомо про нього. Його використовують служби, які збирають загальну статистику про федерації.
         profile_directory: У каталозі профілів перераховані всі користувачі, які погодились бути видимими.
         require_invite_text: Якщо реєстрація вимагає власноручного затвердження, зробіть текстове поле «Чому ви хочете приєднатися?» обов'язковим, а не додатковим
@@ -135,14 +137,17 @@ uk:
         text: Можна структурувати за допомогою синтаксису Markdown.
       terms_of_service_generator:
         admin_email: Юридичні повідомлення містять зустрічні повідомлення, ухвали суду, запити на видалення та запити правоохоронних органів.
-        arbitration_address: Може бути таким самим, як і фізична адреса вище, або "N/A", якщо використано електронну пошту
-        arbitration_website: Може бути вебформою або "N/A", якщо використано електронну пошту
+        arbitration_address: Може бути таким самим, як і фізична адреса вище, або "N/A", якщо використано електронну пошту.
+        arbitration_website: Може бути вебформою або "N/A", якщо використано електронну пошту.
+        choice_of_law: Місто, регіон, територія або держава внутрішні змістовні закони яких повинні керувати будь-якими та всіма твердженнями.
         dmca_address: Для американських операторів використовуйте адресу, зареєстровану в довіднику призначених агентів DMCA. П.О. Перелік скриньок доступний за прямим запитом. Скористайтеся запитом на відмову від поштової скриньки призначеного агента Закону про захист авторських прав у цифрову епоху, щоб надіслати електронний лист до Бюро авторських прав, і опишіть, що ви домашній модератор вмісту, який боїться помсти чи відплати за свої дії та потребує використання P.O. Box, щоб видалити вашу домашню адресу з публічного перегляду.
-        dmca_email: Це може бути та сама адреса електронної пошти, яку використано в розділі «Електронна адреса для юридичних повідомлень» вище
+        dmca_email: Це може бути та сама адреса електронної пошти, яку використано в розділі «Електронна адреса для юридичних повідомлень» вище.
         domain: Унікальна ідентифікація онлайн-сервісу, який ви надаєте.
         jurisdiction: Укажіть країну, де живе той, хто платить за рахунками. Якщо це компанія чи інша організація, вкажіть країну, де вона зареєстрована, а також місто, регіон, територію чи штат відповідно.
+        min_age: Не повинно бути нижче мінімального віку, необхідного законодавством вашої юрисдикції.
       user:
         chosen_languages: У глобальних стрічках будуть показані дописи тільки вибраними мовами
+        date_of_birth: Ми повинні переконатися, що вам принаймні %{age}, щоб використовувати Mastodon. Ми не будемо зберігати це.
         role: Роль визначає, які права має користувач.
       user_role:
         color: Колір, який буде використовуватися для ролі у всьому інтерфейсі, як RGB у форматі hex
@@ -232,6 +237,7 @@ uk:
         setting_display_media_show_all: Показати всі
         setting_expand_spoilers: Завжди розгортати дописи з попередженнями про вміст
         setting_hide_network: Сховати вашу мережу
+        setting_missing_alt_text_modal: Запитувати перед розміщенням медіа без альтернативного тексту
         setting_reduce_motion: Менше руху в анімаціях
         setting_system_font_ui: Використовувати типовий системний шрифт
         setting_system_scrollbars_ui: Використовувати системну панель гортання
@@ -253,6 +259,7 @@ uk:
         name: Хештеґ
       filters:
         actions:
+          blur: Приховати медіа з попередженням
           hide: Сховати повністю
           warn: Сховати за попередженням
       form_admin_settings:
@@ -266,6 +273,7 @@ uk:
         favicon: Піктограма сайту
         mascot: Користувацький символ (застарілий)
         media_cache_retention_period: Період збереження кешу медіа
+        min_age: Мінімальна вимога по віку
         peers_api_enabled: Опублікувати список знайдених серверів у API
         profile_directory: Увімкнути каталог профілів
         registrations_mode: Хто може зареєструватися
@@ -336,11 +344,16 @@ uk:
         admin_email: Адреса електронної пошти для юридичних повідомлень
         arbitration_address: Фізична адреса для арбітражних повідомлень
         arbitration_website: Сайт для надсилання арбітражних повідомлень
+        choice_of_law: Вибір права
         dmca_address: Фізична адреса для сповіщень про DMCA/авторські права
         dmca_email: Фізична адреса для сповіщень про DMCA/авторські права
         domain: Домен
         jurisdiction: Правова юрисдикція
+        min_age: Мінімальний вік
       user:
+        date_of_birth_1i: День
+        date_of_birth_2i: Місяць
+        date_of_birth_3i: Рік
         role: Роль
         time_zone: Часовий пояс
       user_role:
diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml
index f0015e25a6..8b78787f86 100644
--- a/config/locales/simple_form.vi.yml
+++ b/config/locales/simple_form.vi.yml
@@ -75,6 +75,7 @@ vi:
       filters:
         action: Chọn hành động sẽ thực hiện khi một tút khớp với bộ lọc
         actions:
+          blur: Ẩn sau một cảnh báo, mà không ảnh hưởng nội dung
           hide: Ẩn hoàn toàn, như thể nó không tồn tại
           warn: Hiện cảnh báo và bộ lọc
       form_admin_settings:
@@ -88,6 +89,7 @@ vi:
         favicon: WEBP, PNG, GIF hoặc JPG. Dùng favicon Maston tùy chỉnh.
         mascot: Ghi đè hình minh họa trong giao diện web nâng cao.
         media_cache_retention_period: Các tệp phương tiện từ các tút do người dùng máy chủ khác thực hiện sẽ được lưu vào bộ đệm trên máy chủ của bạn. Khi được đặt thành giá trị dương, phương tiện sẽ bị xóa sau số ngày được chỉ định. Nếu dữ liệu phương tiện được yêu cầu sau khi bị xóa, dữ liệu đó sẽ được tải xuống lại nếu nội dung nguồn vẫn còn. Do những hạn chế về tần suất thẻ xem trước liên kết thăm dò ý kiến ​​các trang web của bên thứ ba, bạn nên đặt giá trị này thành ít nhất 14 ngày, nếu không thẻ xem trước liên kết sẽ không được cập nhật theo yêu cầu trước thời gian đó.
+        min_age: Thành viên sẽ được yêu cầu xác nhận ngày sinh của họ trong quá trình đăng ký
         peers_api_enabled: Danh sách các máy chủ khác mà máy chủ này đã liên hợp. Không có dữ liệu nào được đưa vào đây về việc bạn có liên kết với một máy chủ nhất định hay không, chỉ là máy chủ của bạn biết về nó. Điều này được sử dụng bởi các dịch vụ thu thập số liệu thống kê về liên kết theo nghĩa chung.
         profile_directory: Liệt kê tất cả người đã chọn tham gia để có thể khám phá.
         require_invite_text: Khi đăng ký yêu cầu phê duyệt thủ công, hãy đặt câu hỏi "Tại sao bạn muốn tham gia?" nhập văn bản bắt buộc thay vì tùy chọn
@@ -132,17 +134,21 @@ vi:
         name: Bạn có thể thay đổi cách viết hoa các chữ cái để giúp nó dễ đọc hơn
       terms_of_service:
         changelog: Có thể dùng cú pháp Markdown.
+        effective_date: Khung thời gian hợp lý có thể dao động từ 10 đến 30 ngày kể từ ngày bạn thông báo cho người dùng.
         text: Có thể dùng cú pháp Markdown.
       terms_of_service_generator:
         admin_email: Thông báo pháp lý bao gồm thông báo phản đối, lệnh của tòa án, yêu cầu gỡ bỏ và yêu cầu của cơ quan thực thi pháp luật.
-        arbitration_address: Có thể giống với Địa chỉ liên hệ ở trên hoặc “N/A” nếu sử dụng email
-        arbitration_website: Có thể dùng biểu mẫu online, hoặc “N/A” nếu dùng email
+        arbitration_address: Có thể giống với Địa chỉ liên hệ ở trên hoặc “N/A” nếu sử dụng email.
+        arbitration_website: Có thể dùng biểu mẫu online, hoặc “N/A” nếu dùng email.
+        choice_of_law: Thành phố, khu vực, lãnh thổ hoặc tiểu bang có luật riêng sẽ chi phối mọi khiếu nại.
         dmca_address: Đối với các quản trị viên tại Hoa Kỳ, hãy sử dụng địa chỉ đã đăng ký trong Danh bạ đại lý được chỉ định của DMCA. Danh sách hộp thư bưu điện có sẵn khi yêu cầu trực tiếp, hãy sử dụng Yêu cầu miễn trừ hộp thư bưu điện của đại lý được chỉ định của DMCA để gửi email đến Văn phòng bản quyền và mô tả rằng bạn là người kiểm duyệt nội dung tại nhà, người sợ bị trả thù hoặc trừng phạt vì hành động của mình và cần sử dụng hộp thư bưu điện để xóa địa chỉ nhà của bạn khỏi chế độ xem công khai.
-        dmca_email: Có thể là cùng một email được sử dụng cho “Địa chỉ email để nhận thông báo pháp lý” ở trên
+        dmca_email: Có thể là cùng một email được sử dụng cho “Địa chỉ email để nhận thông báo pháp lý” ở trên.
         domain: Mã nhận dạng duy nhất của dịch vụ trực tuyến mà bạn đang cung cấp.
         jurisdiction: Liệt kê quốc gia nơi người trả hóa đơn sinh sống. Nếu đó là công ty hoặc tổ chức khác, hãy liệt kê quốc gia nơi công ty được thành lập và thành phố, khu vực, lãnh thổ hoặc tiểu bang nếu phù hợp.
+        min_age: Không được dưới độ tuổi tối thiểu theo quy định của luật pháp tại khu vực của bạn.
       user:
         chosen_languages: Chỉ hiển thị những tút viết bằng các ngôn ngữ sau
+        date_of_birth: Chúng tôi phải đảm bảo rằng bạn ít nhất %{age} tuổi để sử dụng Mastodon. Chúng tôi không lưu trữ thông tin này.
         role: Vai trò kiểm soát những quyền mà người dùng có.
       user_role:
         color: Màu được sử dụng cho vai trò trong toàn bộ giao diện người dùng, dưới dạng RGB ở định dạng hex
@@ -233,6 +239,7 @@ vi:
         setting_display_media_show_all: Hiện toàn bộ
         setting_expand_spoilers: Luôn mở rộng tút chứa nội dung ẩn
         setting_hide_network: Ẩn quan hệ của bạn
+        setting_missing_alt_text_modal: Hiện xác nhận trước khi đăng media không có văn bản thay thế
         setting_reduce_motion: Giảm chuyển động ảnh GIF
         setting_system_font_ui: Dùng phông chữ mặc định hệ thống
         setting_system_scrollbars_ui: Dùng scrollbar mặc định hệ thống
@@ -254,6 +261,7 @@ vi:
         name: Hashtag
       filters:
         actions:
+          blur: Ẩn kèm theo cảnh báo
           hide: Ẩn toàn bộ
           warn: Ẩn kèm theo cảnh báo
       form_admin_settings:
@@ -267,6 +275,7 @@ vi:
         favicon: Favicon
         mascot: Tùy chỉnh linh vật (kế thừa)
         media_cache_retention_period: Thời hạn lưu trữ cache media
+        min_age: Độ tuổi tối thiểu
         peers_api_enabled: Công khai danh sách các máy chủ được phát hiện trong API
         profile_directory: Cho phép hiện danh sách thành viên
         registrations_mode: Ai có thể đăng ký
@@ -332,16 +341,22 @@ vi:
         usable: Cho phép dùng hashtag này khi soạn tút
       terms_of_service:
         changelog: Điểm mới?
+        effective_date: Ngày có hiệu lực
         text: Điều khoản Dịch vụ
       terms_of_service_generator:
         admin_email: Địa chỉ email để nhận thông báo pháp lý
         arbitration_address: Địa chỉ thực tế để nhận thông báo trọng tài
         arbitration_website: Trang web để nộp thông báo trọng tài
+        choice_of_law: Lựa chọn Luật pháp
         dmca_address: Địa chỉ liên lạc để nhận thông báo DMCA/bản quyền
         dmca_email: Địa chỉ email để nhận thông báo DMCA/bản quyền
         domain: Tên miền
         jurisdiction: Quyền tài phán pháp lý
+        min_age: Độ tuổi tối thiểu
       user:
+        date_of_birth_1i: Ngày
+        date_of_birth_2i: Tháng
+        date_of_birth_3i: Năm
         role: Vai trò
         time_zone: Múi giờ
       user_role:
diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml
index 1c4e8bec31..5fd28497af 100644
--- a/config/locales/simple_form.zh-CN.yml
+++ b/config/locales/simple_form.zh-CN.yml
@@ -88,6 +88,7 @@ zh-CN:
         favicon: WEBP、PNG、GIF 或 JPG。使用自定义图标覆盖 Mastodon 的默认图标。
         mascot: 覆盖高级网页界面中的绘图形象。
         media_cache_retention_period: 来自外站用户嘟文的媒体文件将被缓存到你的实例上。当该值被设为正值时,缓存的媒体文件将在指定天数后被清除。如果媒体文件在被清除后重新被请求,且源站内容仍然可用,它将被重新下载。由于链接预览卡拉取第三方站点的频率受到限制,建议将此值设置为至少 14 天,如果小于该值,链接预览卡将不会按需更新。
+        min_age: 用户注册时必须确认出生日期
         peers_api_enabled: 本站在联邦宇宙中遇到的站点列表。 此处不包含关于您是否与给定站点联合的数据,只是您的实例知道它。 这由收集一般意义上的联合统计信息的服务使用。
         profile_directory: 个人资料目录会列出所有选择可被发现的用户。
         require_invite_text: 当注册需要手动批准时,将“你为什么想要加入?”设为必填项
@@ -132,17 +133,21 @@ zh-CN:
         name: 你只能改变字母的大小写,让它更易读
       terms_of_service:
         changelog: 可以使用 Markdown 语法。
+        effective_date: 合理的时间范围可以是从您通知用户之日起 10 到 30 天。
         text: 可以使用 Markdown 语法。
       terms_of_service_generator:
         admin_email: 法务通知包括反通知、法院命令、内容下架要求与执法机关的要求。
-        arbitration_address: 可以与上面的实际地址相同,若使用邮箱,则填写“N/A”
-        arbitration_website: 可以为网页表单,若使用邮箱,则填写“N/A”
+        arbitration_address: 可以与上面的实际地址相同,如果使用电子邮件则为“N/A”。
+        arbitration_website: 可以是网页表单,如果使用电子邮件则为“N/A”。
+        choice_of_law: 适用内部实质法律以管辖任何及所有索赔的城市、地区、领土或州。
         dmca_address: 如果你是位于美国的运营者,请使用在 DMCA 指定代表名录中注册的地址。如果你需要使用邮政信箱,可以直接申请。请使用 DMCA 指定代表邮政信箱豁免申请表,通过电子邮件联系版权办公室,并声明你是居家内容审核员,因担心审核操作会招致报复或打击报复,需要使用邮政信箱以避免公开家庭住址。
-        dmca_email: 可使用与上方“法务通知接收邮箱地址”相同的邮箱地址。
+        dmca_email: 可以与上面“法律声明的电子邮件地址”使用相同的电子邮件地址。
         domain: 你所提供的在线服务的唯一标识。
         jurisdiction: 请列出支付运营费用者所在的国家/地区。如果为公司或其他实体,请列出其注册的国家/地区以及相应的城市、地区、领地或州。
+        min_age: 不应低于您所在地法律管辖权要求的最低年龄。
       user:
         chosen_languages: 仅选中语言的嘟文会出现在公共时间线上(全不选则显示所有语言的嘟文)
+        date_of_birth: 我们必须确认%{age}岁以上的用户才能使用Mastodon。我们不会存储该信息。
         role: 角色用于控制用户拥有的权限。
       user_role:
         color: 在界面各处用于标记该角色的颜色,以十六进制 RGB 格式表示
@@ -255,6 +260,7 @@ zh-CN:
         name: 话题
       filters:
         actions:
+          blur: 隐藏媒体并显示警告
           hide: 完全隐藏
           warn: 隐藏时显示警告
       form_admin_settings:
@@ -268,6 +274,7 @@ zh-CN:
         favicon: Favicon
         mascot: 自定义吉祥物(旧)
         media_cache_retention_period: 媒体缓存保留期
+        min_age: 最低年龄要求
         peers_api_enabled: 在API中公开的已知实例的服务器的列表
         profile_directory: 启用用户目录
         registrations_mode: 谁可以注册
@@ -333,16 +340,22 @@ zh-CN:
         usable: 允许本站嘟文使用此话题
       terms_of_service:
         changelog: 变更说明
+        effective_date: 生效日期
         text: 服务条款
       terms_of_service_generator:
         admin_email: 接收法务通知的邮箱地址
         arbitration_address: 仲裁通知的实际送达地址
         arbitration_website: 仲裁通知的在线提交入口
+        choice_of_law: 法律适用选择
         dmca_address: 接收DMCA/版权通知的实际地址
         dmca_email: 接收DMCA/版权通知的邮箱地址
         domain: 域名
         jurisdiction: 法律管辖区
+        min_age: 最低年龄
       user:
+        date_of_birth_1i: 日
+        date_of_birth_2i: 月
+        date_of_birth_3i: 年
         role: 角色
         time_zone: 时区
       user_role:
diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml
index 6b33277674..fc86c77b74 100644
--- a/config/locales/simple_form.zh-TW.yml
+++ b/config/locales/simple_form.zh-TW.yml
@@ -75,6 +75,7 @@ zh-TW:
       filters:
         action: 請選擇當嘟文符合該過濾器時將被執行之動作
         actions:
+          blur: 將多媒體隱藏於警告之後,而不隱藏文字內容
           hide: 完全隱藏過濾內容,當作它似乎不曾存在過
           warn: 隱藏過濾內容於過濾器標題之警告後
       form_admin_settings:
@@ -88,6 +89,7 @@ zh-TW:
         favicon: WEBP、PNG、GIF、或 JPG。使用自訂圖示替代預設 Mastodon favicon 圖示。
         mascot: 覆寫進階網頁介面中的圖例。
         media_cache_retention_period: 來自遠端伺服器嘟文中之多媒體內容將快取於您的伺服器。當設定為正值時,這些多媒體內容將於指定之天數後自您的儲存空間中自動刪除。若多媒體資料於刪除後被請求,且原始內容仍可存取,它們將被重新下載。由於連結預覽中第三方網站查詢頻率限制,建議將其設定為至少 14 日,否則於此之前連結預覽將不被即時更新。
+        min_age: 使用者將於註冊時被要求確認他們的生日
         peers_api_enabled: 浩瀚聯邦宇宙中與此伺服器曾經擦肩而過的網域列表。不包含關於您是否與此伺服器是否有與之串連,僅僅表示您的伺服器已知此網域。這是供收集聯邦宇宙中一般性統計資料服務使用。
         profile_directory: 個人檔案目錄將會列出那些有選擇被發現的使用者。
         require_invite_text: 如果已設定為手動審核註冊,請將「為什麼想要加入呢?」設定為必填項目。
@@ -132,17 +134,21 @@ zh-TW:
         name: 您只能變更大小寫,例如,以使其更易讀。
       terms_of_service:
         changelog: 能以 Markdown 語法撰寫。
+        effective_date: 合理時間範圍可為自您通知使用者之日起的 10 至 30 天。
         text: 能以 Markdown 語法撰寫。
       terms_of_service_generator:
         admin_email: 法律通知包含反駁通知、法院命令、刪除請求和執法單位請求。
-        arbitration_address: 能與上述相同之實體地址,或「N/A」如使用 email
-        arbitration_website: 能為網路表單,或「N/A」如使用 email
+        arbitration_address: 能與上述相同之實體地址,或「N/A」如使用 email。
+        arbitration_website: 能為網路表單,或「N/A」如使用 email。
+        choice_of_law: 城市、區域、領土或州的內部實體法律應規範任何及所有權利。
         dmca_address: 位於美國的運營團隊,請使用於 DMCA 指定代理目錄中註冊之地址。可直接請求 PO Box 清單,使用 DMCA 指定代理郵局信箱豁免請求向版權局發送電子郵件,並描述您是一名在家工作之內容管理員,擔心您的行為將遭報復,需要使用 PO Box 保護您的私人住址。
-        dmca_email: 能使用上述用於「法律通知用途電子郵件」之相同電子郵件
+        dmca_email: 能使用上述用於「法律通知用途電子郵件」之相同電子郵件。
         domain: 您所提供線上服務之唯一識別。
         jurisdiction: 列出帳單支付人之居住國家。若為公司或其他實體,請列出其註冊地所在的國家,及城市、地區、領土、或州別等。
+        min_age: 不應低於您所屬法律管轄區要求之最低年齡。
       user:
         chosen_languages: 當選取時,只有選取語言之嘟文會於公開時間軸中顯示
+        date_of_birth: 我們必須確認您至少年滿 %{age} 以使用 Mastodon。我們不會儲存此資料。
         role: 角色控制使用者有哪些權限。
       user_role:
         color: 於整個使用者介面中用於角色的顏色,十六進位格式的 RGB
@@ -255,6 +261,7 @@ zh-TW:
         name: "「#」主題標籤"
       filters:
         actions:
+          blur: 將多媒體隱藏於警告之後
           hide: 完全隱藏
           warn: 隱藏於警告之後
       form_admin_settings:
@@ -268,6 +275,7 @@ zh-TW:
         favicon: 網站圖示 (Favicon)
         mascot: 自訂吉祥物 (legacy)
         media_cache_retention_period: 多媒體快取資料保留期間
+        min_age: 最低年齡要求
         peers_api_enabled: 於 API 中公開已知伺服器的列表
         profile_directory: 啟用個人檔案目錄
         registrations_mode: 誰能註冊
@@ -333,16 +341,22 @@ zh-TW:
         usable: 允許嘟文使用此主題標籤
       terms_of_service:
         changelog: 有何異動?
+        effective_date: 生效日期
         text: 服務條款
       terms_of_service_generator:
         admin_email: 法律通知用途電子郵件
         arbitration_address: 仲裁通知之實體地址
         arbitration_website: 提交仲裁通知之網站
+        choice_of_law: 法律選擇
         dmca_address: DMCA 或版權通知之實體地址
         dmca_email: DMCA 或版權通知之電子郵件地址
         domain: 網域
         jurisdiction: 司法管轄區
+        min_age: 最低年齡
       user:
+        date_of_birth_1i: 日
+        date_of_birth_2i: 月
+        date_of_birth_3i: 年
         role: 角色
         time_zone: 時區
       user_role:
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index e7fb218e2d..8ae14231ca 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -25,10 +25,12 @@ sk:
       one: Príspevok
       other: Príspevkov
     posts_tab_heading: Príspevky
+    self_follow_error: Nieje povolené nasledovať svoj vlastný účet
   admin:
     account_actions:
       action: Vykonaj
       already_silenced: Tento účet už bol obmedzený.
+      already_suspended: Tento účet už bol vylúčený.
       title: Vykonaj moderovací úkon voči %{acct}
     account_moderation_notes:
       create: Zanechaj poznámku
@@ -246,6 +248,7 @@ sk:
         destroy_custom_emoji_html: "%{name} vymazal/a emotikonu %{target}"
         destroy_domain_allow_html: "%{name} zakázal/a federáciu s doménou %{target}"
         destroy_domain_block_html: "%{name} odblokoval/i doménu %{target}"
+        destroy_email_domain_block_html: "%{name} odblokoval/a emailovú doménu %{target}"
         destroy_ip_block_html: "%{name} vymazal/a pravidlo pre IP %{target}"
         destroy_status_html: "%{name} zmazal/a príspevok od %{target}"
         destroy_unavailable_domain_html: "%{name} znova spustil/a doručovanie pre doménu %{target}"
@@ -360,6 +363,7 @@ sk:
         cancel: Zruš
         confirm: Vylúč
         preamble_html: Chystáš sa vylúčiť <strong>%{domain}</strong> a jej poddomény.
+        stop_communication: Tvoj server prestane komunikovať s týmito servermi.
         title: Potvrď blokovanie domény %{domain}
       created_msg: Doména je v štádiu blokovania
       destroyed_msg: Blokovanie domény bolo zrušené
@@ -391,6 +395,7 @@ sk:
     email_domain_blocks:
       add_new: Pridaj nový
       allow_registrations_with_approval: Povoľ registrovanie so schválením
+      created_msg: Úspešne zablokovaná emailová doména
       delete: Vymaž
       dns:
         types:
@@ -399,6 +404,7 @@ sk:
       new:
         create: Pridaj doménu
         resolve: Preveď doménu
+        title: Blokovať novú emailovú doménu
       not_permitted: Nepovolená
       resolved_through_html: Prevedená cez %{domain}
       title: Blokované e-mailové domény
@@ -1365,6 +1371,7 @@ sk:
       explanation: Tu nájdeš nejaké tipy do začiatku
       feature_action: Zisti viac
       follow_action: Nasleduj
+      follow_title: Prispôsob svoj domáci kanál
       follows_title: Koho nasledovať
       post_title: Vytvor svoj prvý príspevok
       share_action: Zdieľaj
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index e6a6c4a08b..b048dbf728 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -25,9 +25,12 @@ sl:
       other: Objav
       two: Objavi
     posts_tab_heading: Objave
+    self_follow_error: Ni dovoljeno slediti lastnemu računu
   admin:
     account_actions:
       action: Izvedi dejanje
+      already_silenced: Ta račun je že omejen.
+      already_suspended: Ta račun je že suspendiran.
       title: Izvedi moderirano dejanje za %{acct}
     account_moderation_notes:
       create: Pusti opombo
@@ -49,6 +52,7 @@ sl:
         title: Spremeni e-naslov za %{username}
       change_role:
         changed_msg: Vloga uspešno spremenjena!
+        edit_roles: Upravljaj z uporabniškimi vlogami
         label: Spremeni vlogo
         no_role: Brez vloge
         title: Spremeni vlogo za %{username}
@@ -189,6 +193,7 @@ sl:
         create_domain_block: Ustvari blokado domene
         create_email_domain_block: Ustvari blokado domene e-pošte
         create_ip_block: Ustvari pravilo IP
+        create_relay: Ustvari rele
         create_unavailable_domain: Ustvari domeno, ki ni na voljo
         create_user_role: Ustvari vlogo
         demote_user: Ponižaj uporabnika
@@ -200,18 +205,22 @@ sl:
         destroy_email_domain_block: Izbriši blokado domene e-pošte
         destroy_instance: Očisti domeno
         destroy_ip_block: Izbriši pravilo IP
+        destroy_relay: Izbriši rele
         destroy_status: Izbriši objavo
         destroy_unavailable_domain: Izbriši nedosegljivo domeno
         destroy_user_role: Uniči vlogo
         disable_2fa_user: Onemogoči
         disable_custom_emoji: Onemogoči emotikon po meri
+        disable_relay: Onemogoči rele
         disable_sign_in_token_auth_user: Onemogoči overjanje z žetonom po e-pošti za uporabnika
         disable_user: Onemogoči uporabnika
         enable_custom_emoji: Omogoči emotikon po meri
+        enable_relay: Omogoči rele
         enable_sign_in_token_auth_user: Omogoči overjanje z žetonom po e-pošti za uporabnika
         enable_user: Omogoči uporabnika
         memorialize_account: Spomenificiraj račun
         promote_user: Povišaj uporabnika
+        publish_terms_of_service: Objavi pogoje uporabe
         reject_appeal: Zavrni pritožbo
         reject_user: Zavrni uporabnika
         remove_avatar_user: Odstrani avatar
@@ -249,6 +258,7 @@ sl:
         create_domain_block_html: "%{name} je blokiral/a domeno %{target}"
         create_email_domain_block_html: "%{name} je dal/a na črni seznam e-pošto domene %{target}"
         create_ip_block_html: "%{name} je ustvaril/a pravilo za IP %{target}"
+        create_relay_html: "%{name} je ustvaril/a rele %{target}"
         create_unavailable_domain_html: "%{name} je prekinil/a dostavo v domeno %{target}"
         create_user_role_html: "%{name} je ustvaril/a vlogo %{target}"
         demote_user_html: "%{name} je ponižal/a uporabnika %{target}"
@@ -260,18 +270,22 @@ sl:
         destroy_email_domain_block_html: "%{name} je odblokiral/a e-pošto domene %{target}"
         destroy_instance_html: "%{name} je očistil/a domeno %{target}"
         destroy_ip_block_html: "%{name} je izbrisal/a pravilo za IP %{target}"
+        destroy_relay_html: "%{name} je izbrisal/a rele %{target}"
         destroy_status_html: "%{name} je odstranil/a objavo uporabnika %{target}"
         destroy_unavailable_domain_html: "%{name} je nadaljeval/a dostav v domeno %{target}"
         destroy_user_role_html: "%{name} je izbrisal/a vlogo %{target}"
         disable_2fa_user_html: "%{name} je onemogočil/a dvofaktorsko zahtevo za uporabnika %{target}"
         disable_custom_emoji_html: "%{name} je onemogočil/a emotikone %{target}"
+        disable_relay_html: "%{name} je onemogočil/a rele %{target}"
         disable_sign_in_token_auth_user_html: "%{name} je onemogočil/a overjanje z žetonom po e-pošti za uporabnika %{target}"
         disable_user_html: "%{name} je onemogočil/a prijavo za uporabnika %{target}"
         enable_custom_emoji_html: "%{name} je omogočil/a emotikone %{target}"
+        enable_relay_html: "%{name} je omogočil rele %{target}"
         enable_sign_in_token_auth_user_html: "%{name} je omogočil/a overjanje z žetonom po e-pošti za uporabnika %{target}"
         enable_user_html: "%{name} je omogočil/a prijavo za uporabnika %{target}"
         memorialize_account_html: "%{name} je spremenil/a račun uporabnika %{target} v spominsko stran"
         promote_user_html: "%{name} je povišal/a uporabnika %{target}"
+        publish_terms_of_service_html: "%{name} je posodobil/a pogoje uporabe"
         reject_appeal_html: "%{name} je zavrnil/a pritožbo uporabnika %{target} na moderatorsko odločitev"
         reject_user_html: "%{name} je zavrnil/a registracijo iz %{target}"
         remove_avatar_user_html: "%{name} je odstranil podobo (avatar) uporabnika %{target}"
@@ -301,6 +315,7 @@ sl:
       title: Dnevnik revizije
       unavailable_instance: "(ime domene ni na voljo)"
     announcements:
+      back: Nazaj na oznanila
       destroyed_msg: Obvestilo je bilo uspešno izbrisano!
       edit:
         title: Uredi obvestilo
@@ -309,6 +324,9 @@ sl:
       new:
         create: Ustvari obvestilo
         title: Novo obvestilo
+      preview:
+        explanation_html: 'E-poštno sporočilo bo poslano <strong>%{display_count} uporabnikom</strong>. Priloženo bo naslednje besedilo:'
+        title: Pokaži predogled oznanila
       publish: Objavi
       published_msg: Obvestilo je bilo uspešno objavljeno!
       scheduled_for: Načrtovano ob %{time}
@@ -486,6 +504,9 @@ sl:
       title: Sledi priporočilom
       unsuppress: Obnovi sledenje priporočilom
     instances:
+      audit_log:
+        title: Nedavni revizijski zapisi
+        view_all: Prikaži ves revizijski dnevnik
       availability:
         description_html:
           few: Če dostava v domeno spodleti <strong>%{count} različne dni</strong> brez uspeha, ne bo nadaljnjih poskusov dostopa, razen če je prejeta dostava <em>iz</em> domene.
@@ -622,6 +643,7 @@ sl:
         suspend_description_html: Račun in vsa njegova vsebina ne bo dostopna in bo postopoma izbrisana, interakcija z njim pa ne bo več možna. Dejanje je moč povrniti v roku 30 dni. Zaključi vse prijave zoper ta račun.
       actions_description_html: Odločite se, katere ukrepe boste sprejeli za rešitev te prijave. Če sprejmete kazenski ukrep proti prijavljenemu računu, mu bo poslano e-poštno obvestilo, razen če je izbrana kategorija <strong>Neželena pošta</strong>.
       actions_description_remote_html: Odločite se za dejanje, ki bo odločilo o tej prijavi. To bo vplivalo le na to, kako <strong>vaš</strong> strežnik komunicira s tem oddaljenim računom in obravnava njegovo vsebino.
+      actions_no_posts: To poročilo ni vezano na nobene objave, ki bi jih lahko izbrisali
       add_to_report: Dodaj več v prijavo
       already_suspended_badges:
         local: Že suspendiran na tem strežniku
@@ -838,6 +860,7 @@ sl:
       back_to_account: Nazaj na stran računa
       back_to_report: Nazaj na stran prijave
       batch:
+        add_to_report: 'Dodaj poročilu #%{id}'
         remove_from_report: Odstrani iz prijave
         report: Poročaj
       contents: Vsebina
@@ -849,12 +872,17 @@ sl:
       media:
         title: Mediji
       metadata: Metapodatki
+      no_history: Ta objava ni bila spremenjena
       no_status_selected: Nobena objava ni bila spremenjena, ker ni bila nobena izbrana
       open: Odpri objavo
       original_status: Izvorna objava
       reblogs: Ponovljeni blogi
+      replied_to_html: V odgovor %{acct_link}
       status_changed: Objava spremenjena
+      status_title: Avtor/ica objave @%{name}
+      title: Objave računa - @%{name}
       trending: V trendu
+      view_publicly: Prikaži javno
       visibility: Vidnost
       with_media: Z mediji
     strikes:
@@ -896,6 +924,9 @@ sl:
         message_html: Nobenih pravil strežnika niste določili.
       sidekiq_process_check:
         message_html: Noben proces Sidekiq ne poteka za %{value} vrst. Preglejte svojo prilagoditev Sidekiq
+      software_version_check:
+        action: Oglejte si razpoložljive posodobitve
+        message_html: Na voljo je posodobitev Mastodona.
       software_version_critical_check:
         action: Glejte razpoložljive posodobitve
         message_html: Na voljo je kritična posodobitev Mastodona. Posodobite čim prej.
@@ -922,16 +953,42 @@ sl:
       name: Ime
       newest: Najnovejše
       oldest: Najstarejše
+      open: Prikaži javno
       reset: Ponastavi
       review: Stanje pregleda
       search: Išči
       title: Ključniki
       updated_msg: Nastavitve ključnikov uspešno posodobljene
     terms_of_service:
+      back: Nazaj na pogoje uporabe
+      changelog: Kaj je novega
+      create: Uporabi svoje
+      current: Trenutni
       draft: Osnutek
       generate: Uporabi predlogo
+      generates:
+        action: Generiraj
+        chance_to_review_html: "<strong>Generirani pogoji uporabe ne bodo objavljeni samodejno,</strong> tako da jih boste imeli čas preveriti. Vnesite vse potrebne podatke."
+        explanation_html: Predloga pogojev uporabe je informativne narave in naj ne služi kot pravno vodilo. O pravnih vprašanjih in specifikah se posvetujte s pravnim strokovnjakom.
+        title: Postavitev pogojev uporabe
+      going_live_on_html: Objavljeni, začnejo veljati %{date}
       history: Zgodovina
+      live: Objavljeni
+      no_history: V pogojih uporabe še ni zabeleženih sprememb.
+      no_terms_of_service_html: Trenutno nimate nastavljenih pogojev uporabe. Ti naj bi razjasnili pravno razmerje in dodeljevanje odgovornosti med vami in vašimi uporabniki v primeru spora.
+      notified_on_html: Uporabniki so obveščeni %{date}
+      notify_users: Obvesti uporabnike
+      preview:
+        explanation_html: 'E-poštno sporočilo bo poslano <strong>%{display_count} uporabnikom</strong>, ki so se registrirali pred %{date}. Priloženo bo naslednje besedilo:'
+        send_preview: Pošlji predogled na %{email}
+        send_to_all:
+          few: Pošlji %{display_count} e-poštna sporočila
+          one: Pošlji %{display_count} e-poštno sporočilo
+          other: Pošlji %{display_count} e-poštnih sporočil
+          two: Pošlji %{display_count} e-poštni sporočili
+        title: Prikaži predogled obvestila pogojev uporabe
       publish: Objavi
+      published_on_html: Objavljeno %{date}
       save_draft: Shrani osnutek
       title: Pogoji uporabe
     title: Upravljanje
@@ -1173,6 +1230,7 @@ sl:
     set_new_password: Nastavi novo geslo
     setup:
       email_below_hint_html: Poglejte v mapo neželene pošte ali zaprosite za novega. Če ste podali napačen e-naslov, ga lahko popravite.
+      email_settings_hint_html: Kliknite na povezavo, ki smo vam jo poslali na %{email}, pa boste lahko začeli uporabljati Mastodon. Tukajle bomo počakali.
       link_not_received: Ali ste prejeli povezavo?
       new_confirmation_instructions_sent: Čez nekaj minut boste prejeli novo e-sporočilo s potrditveno povezavo!
       title: Preverite svojo dohodno e-pošto
@@ -1181,6 +1239,7 @@ sl:
       title: Vpiši se v %{domain}
     sign_up:
       manual_review: Registracije na %{domain} ročno pregledajo naši moderatorji. Da nam olajšate obdelavo vaše prijave, zapišite kaj o sebi in zakaj si želite račun na %{domain}.
+      preamble: Če ustvarite račun na tem strežniku Mastodona, boste lahko sledili komur koli v fediverzumu, ne glede na to, kje gostuje njegov/njen račun.
       title: Naj vas namestimo na %{domain}.
     status:
       account_status: Stanje računa
@@ -1192,8 +1251,16 @@ sl:
       view_strikes: Pokaži pretekle ukrepe proti mojemu računu
     too_fast: Obrazec oddan prehitro, poskusite znova.
     use_security_key: Uporabi varnostni ključ
+    user_agreement_html: Prebral/a sem <a href="%{terms_of_service_path}" target="_blank">pogoje uporabe</a> in <a href="%{privacy_policy_path}" target="_blank">politiko zasebnosti</a> in z obojim soglašam
+    user_privacy_agreement_html: Prebral sem <a href="%{privacy_policy_path}" target="_blank">politiko zasebnosti</a> in soglašam z njo
   author_attribution:
     example_title: Vzorčno besedilo
+    hint_html: Ali pišete novičke ali spletni dnevnik kje drugje poleg Mastodona? Poskrbite, da bo vaše avtorstvo pravilno navedeno, ko bo kdo delil vaše delo na Mastodonu.
+    instructions: 'Poskrbite, da bo v dokumentu HTML vašega prispevka naslednja koda:'
+    more_from_html: Več od %{name}
+    s_blog: Spletni dnevnik %{name}
+    then_instructions: Nato dodajte ime domene, kamor objavljate, v spodnje polje.
+    title: Priznanje avtorstva
   challenge:
     confirm: Nadaljuj
     hint_html: "<strong>Namig:</strong> naslednjo uro vas ne bomo več vprašali po vašem geslu."
@@ -1404,6 +1471,68 @@ sl:
       merge_long: Ohrani obstoječe zapise in dodaj nove
       overwrite: Prepiši
       overwrite_long: Zamenjaj trenutne zapise z novimi
+    overwrite_preambles:
+      blocking_html:
+        few: Kaže, da želite <strong>zamenjati svoj seznam blokiranih</strong> z do <strong>%{count} računi</strong> iz <strong>%{filename}</strong>.
+        one: Kaže, da želite <strong>zamenjati svoj seznam blokiranih</strong> z do <strong>%{count} računom</strong> iz <strong>%{filename}</strong>.
+        other: Kaže, da želite <strong>zamenjati svoj seznam blokiranih</strong> z do <strong>%{count} računi</strong> iz <strong>%{filename}</strong>.
+        two: Kaže, da želite <strong>zamenjati svoj seznam blokiranih</strong> z do <strong>%{count} računoma</strong> iz <strong>%{filename}</strong>.
+      bookmarks_html:
+        few: Kaže, da želite <strong>zamenjati svoje zaznamke</strong> z do <strong>%{count} objavami</strong> iz <strong>%{filename}</strong>.
+        one: Kaže, da želite <strong>zamenjati svoje zaznamke</strong> z do <strong>%{count} objavo</strong> iz <strong>%{filename}</strong>.
+        other: Kaže, da želite <strong>zamenjati svoje zaznamke</strong> z do <strong>%{count} objavami</strong> iz <strong>%{filename}</strong>.
+        two: Kaže, da želite <strong>zamenjati svoje zaznamke</strong> z do <strong>%{count} objavama</strong> iz <strong>%{filename}</strong>.
+      domain_blocking_html:
+        few: Kaže, da želite <strong>zamenjati svoj seznam blokiranih domen</strong> z do <strong>%{count} domenami</strong> iz <strong>%{filename}</strong>.
+        one: Kaže, da želite <strong>zamenjati svoj seznam blokiranih domen</strong> z do <strong>%{count} domeno</strong> iz <strong>%{filename}</strong>.
+        other: Kaže, da želite <strong>zamenjati svoj seznam blokiranih domen</strong> z do <strong>%{count} domenami</strong> iz <strong>%{filename}</strong>.
+        two: Kaže, da želite <strong>zamenjati svoj seznam blokiranih domen</strong> z do <strong>%{count} domenama</strong> iz <strong>%{filename}</strong>.
+      following_html:
+        few: Kaže, da želite <strong>slediti</strong> do <strong>%{count} računom</strong> iz <strong>%{filename}</strong> in <strong>nehati slediti komur koli drugemu</strong>.
+        one: Kaže, da želite <strong>slediti</strong> do <strong>%{count} računu</strong> iz <strong>%{filename}</strong> in <strong>nehati slediti komur koli drugemu</strong>.
+        other: Kaže, da želite <strong>slediti</strong> do <strong>%{count} računom</strong> iz <strong>%{filename}</strong> in <strong>nehati slediti komur koli drugemu</strong>.
+        two: Kaže, da želite <strong>slediti</strong> do <strong>%{count} računoma</strong> iz <strong>%{filename}</strong> in <strong>nehati slediti komur koli drugemu</strong>.
+      lists_html:
+        few: Kaže, da nameravate <strong>zamenjati svoje sezname</strong> z vsebino iz <strong>%{filename}</strong>. V nove sezname bodo dodani do <strong>%{count} računi</strong>.
+        one: Kaže, da nameravate <strong>zamenjati svoje sezname</strong> z vsebino iz <strong>%{filename}</strong>. V nove sezname bo dodan do <strong>%{count} račun</strong>.
+        other: Kaže, da nameravate <strong>zamenjati svoje sezname</strong> z vsebino iz <strong>%{filename}</strong>. V nove sezname bo dodanih do <strong>%{count} računov</strong>.
+        two: Kaže, da nameravate <strong>zamenjati svoje sezname</strong> z vsebino iz <strong>%{filename}</strong>. V nove sezname bosta dodana do <strong>%{count} računa</strong>.
+      muting_html:
+        few: Kaže, da nameravate <strong>zamenjati svoj seznam utišanih računov</strong> z do <strong>%{count} računi</strong> iz <strong>%{filename}</strong>.
+        one: Kaže, da nameravate <strong>zamenjati svoj seznam utišanih računov</strong> z do <strong>%{count} računom</strong> iz <strong>%{filename}</strong>.
+        other: Kaže, da nameravate <strong>zamenjati svoj seznam utišanih računov</strong> z do <strong>%{count} računi</strong> iz <strong>%{filename}</strong>.
+        two: Kaže, da nameravate <strong>zamenjati svoj seznam utišanih računov</strong> z do <strong>%{count} računoma</strong> iz <strong>%{filename}</strong>.
+    preambles:
+      blocking_html:
+        few: Kaže, da nameravate <strong>blokirati</strong> <strong>%{count} račune</strong> iz <strong>%{filename}</strong>.
+        one: Kaže, da nameravate <strong>blokirati</strong> <strong>%{count} račun</strong> iz <strong>%{filename}</strong>.
+        other: Kaže, da nameravate <strong>blokirati</strong> <strong>%{count} računov</strong> iz <strong>%{filename}</strong>.
+        two: Kaže, da nameravate <strong>blokirati</strong> <strong>%{count} računa</strong> iz <strong>%{filename}</strong>.
+      bookmarks_html:
+        few: V svoje <strong>zaznamke</strong> boste dodali <strong>%{count} objave</strong> iz <strong>%{filename}</strong>.
+        one: V svoje <strong>zaznamke</strong> boste dodali <strong>%{count} objavo</strong> iz <strong>%{filename}</strong>.
+        other: V svoje <strong>zaznamke</strong> boste dodali <strong>%{count} objav</strong> iz <strong>%{filename}</strong>.
+        two: V svoje <strong>zaznamke</strong> boste dodali <strong>%{count} objavi</strong> iz <strong>%{filename}</strong>.
+      domain_blocking_html:
+        few: "<strong>Blokirali</strong> boste <strong>%{count} domene</strong> iz <strong>%{filename}</strong>."
+        one: "<strong>Blokirali</strong> boste <strong>%{count} domeno</strong> iz <strong>%{filename}</strong>."
+        other: "<strong>Blokirali</strong> boste <strong>%{count} domen</strong> iz <strong>%{filename}</strong>."
+        two: "<strong>Blokirali</strong> boste <strong>%{count} domeni</strong> iz <strong>%{filename}</strong>."
+      following_html:
+        few: Začeli boste <strong>slediti</strong> <strong>%{count} računom</strong> iz <strong>%{filename}</strong>.
+        one: Začeli boste <strong>slediti</strong> <strong>%{count} računu</strong> iz <strong>%{filename}</strong>.
+        other: Začeli boste <strong>slediti</strong> <strong>%{count} računom</strong> iz <strong>%{filename}</strong>.
+        two: Začeli boste <strong>slediti</strong> <strong>%{count} računoma</strong> iz <strong>%{filename}</strong>.
+      lists_html:
+        few: V svoje <strong>sezname</strong> boste dodali <strong>%{count} račune</strong> iz <strong>%{filename}</strong>. Če seznami manjkajo, bodo ustvarjeni.
+        one: V svoje <strong>sezname</strong> boste dodali <strong>%{count} račun</strong> iz <strong>%{filename}</strong>. Če seznami manjkajo, bodo ustvarjeni.
+        other: V svoje <strong>sezname</strong> boste dodali <strong>%{count} računov</strong> iz <strong>%{filename}</strong>. Če seznami manjkajo, bodo ustvarjeni.
+        two: V svoje <strong>sezname</strong> boste dodali <strong>%{count} računa</strong> iz <strong>%{filename}</strong>. Če seznami manjkajo, bodo ustvarjeni.
+      muting_html:
+        few: "<strong>Utišali</strong> boste <strong>%{count} račune</strong> iz <strong>%{filename}</strong>."
+        one: "<strong>Utišali</strong> boste <strong>%{count} račun</strong> iz <strong>%{filename}</strong>."
+        other: "<strong>Utišali</strong> boste <strong>%{count} računov</strong> iz <strong>%{filename}</strong>."
+        two: "<strong>Utišali</strong> boste <strong>%{count} računa</strong> iz <strong>%{filename}</strong>."
     preface: Podatke, ki ste jih izvozili iz drugega strežnika, lahko uvozite. Na primer seznam oseb, ki jih spremljate ali blokirate.
     recent_imports: Nedavni uvozi
     states:
@@ -1490,6 +1619,7 @@ sl:
   media_attachments:
     validations:
       images_and_video: Videoposnetka ni mogoče priložiti objavi, ki že vsebuje slike
+      not_found: Predstavnosti %{ids} ne najdem ali pa je že pripeta k drugi objavi
       not_ready: Datotek, katerih obdelava ni dokončana, ni mogoče pripeti. Poskusite znova kmalu!
       too_many: Ni možno priložiti več kot 4 datoteke
   migrations:
@@ -1661,6 +1791,7 @@ sl:
   scheduled_statuses:
     over_daily_limit: Za ta dan ste presegli omejitev %{limit} načrtovanih objav
     over_total_limit: Presegli ste omejitev %{limit} načrtovanih objav
+    too_soon: datum mora biti v prihodnosti
   self_destruct:
     lead_html: Na žalost se <strong>%{domain}</strong> za vedno zapira. Če ste tu imeli svoj račun, ga v prihodnje ne boste mogli več uporabljati. Zahtevate lahko kopijo svojih podatkov.
     title: Ta strežnik se zapira
@@ -1864,6 +1995,10 @@ sl:
     recovery_instructions_html: Če kdaj izgubite dostop do telefona, lahko uporabite eno od spodnjih obnovitvenih kod, da ponovno pridobite dostop do svojega računa. <strong>Shranite obnovitvene kode</strong>. Lahko jih natisnete in shranite z drugimi pomembnimi dokumenti.
     webauthn: Varnostni ključi
   user_mailer:
+    announcement_published:
+      description: 'Skrbniki domene %{domain} oznanjajo:'
+      subject: Storitveno oznanilo
+      title: Storitveno oznanilo %{domain}
     appeal_approved:
       action: Nastavitve računa
       explanation: Pritožbi na ukrep proti vašemu računu z dne %{strike_date}, ki ste jo oddali dne %{appeal_date}, je bilo ugodeno. Vaš račun je znova nesporen.
@@ -1894,6 +2029,13 @@ sl:
       subject: Do vašega računa je bil opravljen dostop z novega naslova IP
       title: Nova prijava
     terms_of_service_changed:
+      agreement: Če boste še naprej uporabljali %{domain}, se strinjate s temi pogoji. Če se ne, lahko kadarkoli odstopite od dogovora z domeno %{domain} tako, da izbrišete svoj račun.
+      changelog: 'Kratek povzetek tega, kaj ta posodobitev pomeni za vas:'
+      description: 'To e-poštno sporočilo ste prejeli, ker smo spremenili pogoje uporabe v domeni %{domain}. Posodobitve bodo začele veljati %{date}. Vabimo vas, da si posodobljene pogoje preberete tukaj:'
+      description_html: To e-poštno sporočilo ste prejeli, ker smo spremenili pogoje uporabe v domeni %{domain}. Posodobitve bodo začele veljati <strong>%{date}</strong>. Vabimo vas, da si posodobljene pogoje preberete <a href="%{path}" target="_blank">na tej povezavi</a>.
+      sign_off: Ekipa %{domain}
+      subject: Posodobitve naših pogojev uporabe
+      subtitle: Spreminjajo se pogoji uporabe domene %{domain}
       title: Pomembna posodobitev
     warning:
       appeal: Pošlji pritožbo
@@ -1984,6 +2126,7 @@ sl:
     instructions_html: Spodnjo kodo kopirajte in prilepite v HTML svojega spletnega mesta. Nato dodajte naslov svoje spletne strani v eno od dodatnih polj v svojem profilu v zavihku »Uredi profil« in shranite spremembe.
     verification: Potrditev
     verified_links: Vaše preverjene povezave
+    website_verification: Overitev spletišča
   webauthn_credentials:
     add: Dodaj nov varnostni ključ
     create:
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index 0697bb0a99..01884c10e9 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -309,6 +309,7 @@ sq:
       title: Regjistër auditimesh
       unavailable_instance: "(emër përkatësie jo i passhëm)"
     announcements:
+      back: Mbrapsht te njoftimet
       destroyed_msg: Lajmërimi u fshi me sukses!
       edit:
         title: Përpunoni lajmërimin
@@ -317,6 +318,10 @@ sq:
       new:
         create: Krijoni lajmërim
         title: Lajmërim i ri
+      preview:
+        disclaimer: Ngaqë përdoruesit s’mund të zgjedhin lënien jashtë tyre, njoftimet me email do të kufizohen te njoftime të rëndësishme, të tilla si cenim të dhënash personale, ose njoftime mbylljesh shërbyesish.
+        explanation_html: 'Email-i do të dërgohet te <strong>%{display_count} përdorues</strong>. Te email-i do të përfshihet teksti vijues:'
+        title: Bëni paraparje të shënimit për njoftimin
       publish: Publikoje
       published_msg: Lajmërimi u botua me sukses!
       scheduled_for: Vënë në plan për më %{time}
@@ -475,6 +480,32 @@ sq:
       new:
         title: Importoni bllokime përkatësish
       no_file: S’u përzgjodh kartelë
+    fasp:
+      debug:
+        callbacks:
+          created_at: Krijuar më
+          delete: Fshije
+          ip: Adresë IP
+          request_body: Lëndë kërkese
+      providers:
+        active: Aktivë
+        delete: Fshije
+        edit: Përpunoni Shërbim
+        finish_registration: Përfundoje regjistrimin
+        name: Emër
+        providers: Shërbime
+        public_key_fingerprint: Shenja gishtash kyçi publik
+        registration_requested: Lyp regjistrim
+        registrations:
+          confirm: Ripohojeni
+          description: Morët një regjistrim nga një FASP. Nëse s’e filluat ju, hidheni tej. Nëse e filluat ju, krahasoni me kujdes emrin dhe shenjat e gishtave, para se të ripohoni regjistrimin.
+          reject: Hidhe tej
+          title: Ripohoni Regjistrim në FASP
+        save: Ruaje
+        select_capabilities: Përzgjidhni Aftësi
+        sign_in: Hyni
+        status: Gjendje
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Rekomandimet për ndjekje ndihmojnë përdoruesit e rinj të gjejnë shpejt lëndë me interes</strong>. Kur një përdorues nuk ka ndërvepruar mjaftueshëm me të tjerët, që të formohen rekomandime të personalizuara ndjekjeje, rekomandohen këto llogari. Ato përzgjidhen çdo ditë, prej një përzierje llogarish me shkallën më të lartë të angazhimit dhe numrin më të lartë të ndjekësve vendorë për një gjuhë të dhënë."
       language: Për gjuhën
@@ -934,6 +965,7 @@ sq:
         chance_to_review_html: "<strong>Termat e shërbimit të prodhuar s’do të botohen automatikisht.</strong> Do të keni një mundësi të shqyrtoni përfundimet. Ju lutemi, që të vazhdohet, plotësoni hollësitë e nevojshme."
         explanation_html: Gjedhja e dhënë për kushtet e shërbimit është vetëm për qëllime njohjeje dhe s’duhet marrë si këshillë ligjore, për çfarëdo çështje. Ju lutemi, për situatën tuaj dhe pyetje specifike ligjore, lidhuni me juristin tuaj.
         title: Ujdisje Termash Shërbimi
+      going_live_on_html: Funksionalë, në fuqi që prej %{date}
       history: Historik
       live: Drejtpërdrejt
       no_history: Ende s’ka ndryshime të regjistruara të kushteve të shërbimit.
@@ -1898,6 +1930,10 @@ sq:
     recovery_instructions_html: Në ndodhtë që të humbni hyrje te telefoni juaj, mund të përdorni një nga kodet e rikthimit më poshtë, që të rifitoni hyrje te llogaria juaj. <strong>Mbajini të parrezik kodet e rikthimeve</strong>. Për shembull, mund t’i shtypni dhe t’i ruani tok me dokumente të tjerë të rëndësishëm.
     webauthn: Kyçe sigurie
   user_mailer:
+    announcement_published:
+      description: 'Përgjegjësit e %{domain} po bëjnë një njoftim:'
+      subject: Njoftim shërbimi
+      title: Njoftim shërbimi %{domain}
     appeal_approved:
       action: Rregullime Llogarie
       explanation: Apelimi i paralajmërimit kundër llogarisë tuaj më %{strike_date}, të cilin e parashtruar më %{appeal_date} është miratuar. Llogaria juaj është sërish në pozita të mira.
@@ -1930,8 +1966,8 @@ sq:
     terms_of_service_changed:
       agreement: Duke vazhduar të përdorni %{domain}, pajtoheni më këto terma. Nëse s’pajtoheni me termat e përditësuar, mund të përfundoni pajtimin tuaj me %{domain} në çfarëdo kohe, përmes fshirjes së llogarisë tuaj.
       changelog: 'Me një vështrim, ja se ç’do të thotë ky përditësim për ju:'
-      description: 'Po e merrni këtë email ngaqë po bëjmë disa ndryshime te kushtet tona të shërbimit në %{domain}. Ju nxisim të shqyrtoni termat e përditësuar të plotë këtu:'
-      description_html: Po e merrni këtë email ngaqë po bëjmë disa ndryshime në kushtet tona të shërbimit te %{domain}. Ju nxisim të shqyrtoni <a href="%{path}" target="_blank">termat e përditësuar të plotë këtu</a>.
+      description: 'Po e merrni këtë email ngaqë po bëjmë disa ndryshime te kushtet tona të shërbimit te %{domain}. Këto përditësime do të hyjnë në fuqi më %{date}. Ju nxisim të shqyrtoni kushtet e përditësuara të plota këtu:'
+      description_html: Po e merrni këtë email ngaqë po bëjmë disa ndryshime te kushtet tona të shërbimit te %{domain}. Këto përditësime do të hyjnë në fuqi më <strong>%{date}</strong>. Ju nxisim t’i shqyrtoni <a href="%{path}" target="_blank">të plota kushtet e përditësuara këtu</a>.
       sign_off: Ekipi i %{domain}
       subject: Përditësime të termave tanë të shërbimit
       subtitle: Termat e shërbimit të %{domain} po ndryshojnë
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 7855c911d1..bc351a8f3b 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -475,6 +475,19 @@ sv:
       new:
         title: Importera domänblockeringar
       no_file: Ingen fil vald
+    fasp:
+      debug:
+        callbacks:
+          ip: IP-adress
+      providers:
+        base_url: Bas-URL
+        delete: Ta bort
+        finish_registration: Slutför registrering
+        name: Namn
+        registrations:
+          confirm: Bekräfta
+          reject: Avvisa
+        save: Spara
     follow_recommendations:
       description_html: "<strong>Följrekommendationer hjälper nya användare att snabbt hitta intressant innehåll</strong>. När en användare inte har interagerat med andra tillräckligt mycket för att forma personliga följrekommendationer, rekommenderas istället dessa konton. De beräknas om varje dag från en mix av konton med nylig aktivitet och högst antal följare för ett givet språk."
       language: För språket
@@ -1209,6 +1222,7 @@ sv:
     too_fast: Formuläret har skickats för snabbt, försök igen.
     use_security_key: Använd säkerhetsnyckel
     user_agreement_html: Jag har läst och godkänner <a href="%{terms_of_service_path}" target="_blank">användarvillkoren</a> och <a href="%{privacy_policy_path}" target="_blank">integritetspolicy</a>
+    user_privacy_agreement_html: Jag har läst och godkänner <a href="%{privacy_policy_path}" target="_blank">integritetspolicyn</a>
   author_attribution:
     example_title: Exempeltext
     hint_html: Skriver du nyheter eller bloggartiklar utanför Mastodon? Kontrollera hur du får krediteras när de delas på Mastodon.
@@ -1935,8 +1949,6 @@ sv:
     terms_of_service_changed:
       agreement: Genom att fortsätta använda %{domain} godkänner du dessa villkor. Om du inte håller med om de uppdaterade villkoren kan du när som helst säga upp ditt avtal med %{domain} genom att radera ditt konto.
       changelog: 'I korthet, här är vad denna uppdatering innebär för dig:'
-      description: 'Du får detta e-postmeddelande eftersom vi gör vissa ändringar i våra användarvillkor på %{domain}. Vi uppmanar dig att granska de uppdaterade villkoren i sin helhet här:'
-      description_html: Du får detta e-postmeddelande eftersom vi gör vissa ändringar i våra användarvillkor på %{domain}. Vi uppmanar dig att granska de uppdaterade villkoren i <a href="%{path}" target="_blank">sin helhet här</a>.
       sign_off: "%{domain} teamet"
       subject: Uppdateringar till våra användarvillkor
       subtitle: Villkoren för tjänsten på %{domain} ändras
diff --git a/config/locales/th.yml b/config/locales/th.yml
index b945ee8650..19f9e4da1f 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -466,6 +466,12 @@ th:
       new:
         title: นำเข้าการปิดกั้นโดเมน
       no_file: ไม่ได้เลือกไฟล์
+    fasp:
+      debug:
+        callbacks:
+          created_at: สร้างเมื่อ
+          delete: ลบ
+          ip: ที่อยู่ IP
     follow_recommendations:
       description_html: "<strong>คำแนะนำการติดตามช่วยให้ผู้ใช้ใหม่ค้นหาเนื้อหาที่น่าสนใจได้อย่างรวดเร็ว</strong> เมื่อผู้ใช้ไม่ได้โต้ตอบกับผู้อื่นมากพอที่จะสร้างคำแนะนำการติดตามเฉพาะบุคคล จะแนะนำบัญชีเหล่านี้แทน จะคำนวณคำแนะนำใหม่เป็นประจำทุกวันจากบัญชีต่าง ๆ ที่มีการมีส่วนร่วมล่าสุดสูงสุดและจำนวนผู้ติดตามในเซิร์ฟเวอร์สูงสุดสำหรับภาษาที่กำหนด"
       language: สำหรับภาษา
@@ -1089,7 +1095,7 @@ th:
     salutation: "%{name},"
     settings: 'เปลี่ยนการกำหนดลักษณะอีเมล: %{link}'
     unsubscribe: เลิกบอกรับ
-    view: 'มุมมอง:'
+    view: 'ดู:'
     view_profile: ดูโปรไฟล์
     view_status: ดูโพสต์
   applications:
@@ -1177,6 +1183,7 @@ th:
       view_strikes: ดูการดำเนินการที่ผ่านมาต่อบัญชีของคุณ
     too_fast: ส่งแบบฟอร์มเร็วเกินไป ลองอีกครั้ง
     use_security_key: ใช้กุญแจความปลอดภัย
+    user_privacy_agreement_html: ฉันได้อ่านและเห็นด้วยกับ <a href="%{privacy_policy_path}" target="_blank">นโยบายความเป็นส่วนตัว</a>
   author_attribution:
     example_title: ข้อความตัวอย่าง
     hint_html: คุณกำลังเขียนข่าวหรือบทความบล็อกภายนอก Mastodon หรือไม่? ควบคุมวิธีที่คุณได้รับเครดิตเมื่อมีการแบ่งปันข่าวหรือบทความบล็อกใน Mastodon
@@ -1635,7 +1642,7 @@ th:
     last_active: ใช้งานล่าสุด
     most_recent: ล่าสุด
     moved: ย้ายแล้ว
-    mutual: ร่วมกัน
+    mutual: คนที่มีร่วมกัน
     primary: หลัก
     relationship: ความสัมพันธ์
     remove_selected_domains: เอาผู้ติดตามทั้งหมดออกจากโดเมนที่เลือก
diff --git a/config/locales/tok.yml b/config/locales/tok.yml
index 2cb8429ac4..48b5406147 100644
--- a/config/locales/tok.yml
+++ b/config/locales/tok.yml
@@ -27,9 +27,14 @@ tok:
     accounts:
       approve: o wile
       are_you_sure: ni li pona ala pona?
+      avatar: sitelen
       delete: o ala e sona
       deleted: jan li ala e ni
       demote: o lili e ken
+      edit: ante toki
+      invite_request_text: nasin kama
+      ip: nanpa IP
+      joined: tenpo kama
       search: o alasa
     tags:
       search: o alasa
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 7470f88c50..751e6ae22f 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -309,6 +309,7 @@ tr:
       title: Denetim günlüğü
       unavailable_instance: "(alan adı mevcut değil)"
     announcements:
+      back: Duyurulara geri dön
       destroyed_msg: Duyuru başarıyla silindi!
       edit:
         title: Duyuruyu düzenle
@@ -317,6 +318,10 @@ tr:
       new:
         create: Duyuru oluştur
         title: Yeni duyuru
+      preview:
+        disclaimer: Kullanıcılar bu bildirimleri almayı iptal edemediği için, e-posta bildirimleri kişisel veri ihlali veya sunucu kapatma bildirimleri gibi önemli duyurularla sınırlandırılmalıdır.
+        explanation_html: 'E-posta, <strong>%{display_count} kullanıcıya</strong> gönderilecektir. E-posta içerisinde aşağıdaki metin yer alacaktır:'
+        title: Duyuru bildiriminin önizlemesine bak
       publish: Yayınla
       published_msg: Duyuru başarıyla yayınlandı!
       scheduled_for: "%{time} için zamanlandı"
@@ -475,6 +480,36 @@ tr:
       new:
         title: Domain bloklarını içe aktar
       no_file: Dosya seçilmedi
+    fasp:
+      debug:
+        callbacks:
+          created_at: Oluşturulma tarihi
+          delete: Sil
+          ip: IP adresi
+          request_body: İstek gövdesi
+          title: Hata ayıklama geri çağrıları
+      providers:
+        active: Etkin
+        base_url: Temel URL
+        callback: Geri çağırma
+        delete: Sil
+        edit: Sağlayıcıyı Düzenle
+        finish_registration: Kaydı tamamla
+        name: Ad
+        providers: Sağlayıcılar
+        public_key_fingerprint: Açık anahtar parmak izi
+        registration_requested: Kayıt istendi
+        registrations:
+          confirm: Onayla
+          description: FASP'tan bir kayıt aldınız. Eğer bunu siz başlatmadıysanız reddedin. Eğer siz başlattıysanız, kaydı onaylamadan önce ad ve anahtar parmak izini dikkatlice karşılaştırın.
+          reject: Reddet
+          title: FASP Kaydını Onayla
+        save: Kaydet
+        select_capabilities: Yetenekleri Seç
+        sign_in: Oturum Aç
+        status: Durum
+        title: Fediverse Yardımcı Hizmet Sağlayıcıları (FASP)
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Takip önerileri yeni kullanıcıların hızlı bir şekilde ilginç içerik bulmalarını sağlar</strong>. Eğer bir kullanıcı, kişisel takip önerileri almaya yetecek kadar başkalarıyla etkileşime girmediğinde, onun yerine bu hesaplar önerilir. Bu öneriler, verili bir dil için en yüksek takipçi sayısına ve en yüksek güncel meşguliyete sahip hesapların bir karışımdan günlük olarak hesaplanıyorlar."
       language: Dil için
@@ -939,6 +974,7 @@ tr:
         chance_to_review_html: "<strong>Üretilmiş hizmet şartları otomatik olarak yayınlanmayacaktır.</strong> Sonucu gözden geçirme şansınız olacaktır. Devam etmek için lütfen gerekli ayrıntıları doldurun."
         explanation_html: Sağlanan hizmet şartları şablonu yalnızca bilgilendirme amaçlıdır ve herhangi bir konuda yasal tavsiye olarak yorumlanmamalıdır. Lütfen durumunuz ve belirli yasal sorularınız hakkında kendi hukuk müşavirinize danışın.
         title: Hizmet Şartları Kurulumu
+      going_live_on_html: Canlı, %{date} tarihinden beri yürürlükte
       history: Tarihçe
       live: Canlı
       no_history: Henüz kaydedilmiş hizmet şartları değişikliği yok.
@@ -1904,6 +1940,10 @@ tr:
     recovery_instructions_html: 'Eğer telefonunuza erişiminizi kaybederseniz, aşağıdaki kurtarma kodlarından birini kullanarak hesabınıza giriş yapabilirsiniz. <strong>Kurtarma kodlarınızı güvenli halde tutunuz.</strong> Örneğin: kodların çıktısını alıp diğer önemli belgeleriniz ile birlikte saklayabilirsiniz.'
     webauthn: Güvenlik anahtarları
   user_mailer:
+    announcement_published:
+      description: "%{domain} yöneticileri bir duyuru yapıyorlar:"
+      subject: Hizmet duyurusu
+      title: "%{domain} hizmet duyurusu"
     appeal_approved:
       action: Hesap Ayarları
       explanation: "%{appeal_date} tarihinde gönderdiğiniz, hesabınıza yönelik %{strike_date} tarihli eyleme itirazınız onaylandı. Hesabınız artık tekrar iyi durumda."
@@ -1936,8 +1976,8 @@ tr:
     terms_of_service_changed:
       agreement: "%{domain} sunucusunu kullanmaya devam ederek bu şartları kabul etmiş olursunuz. Güncellenen şartları kabul etmiyorsanız, %{domain} ile olan sözleşmenizi istediğiniz zaman hesabınızı silerek feshedebilirsiniz."
       changelog: 'Bir bakışta, bu güncellemenin sizin için anlamı şudur:'
-      description: 'Bu e-postayı alıyorsunuz çünkü %{domain} adresindeki hizmet şartlarımızda bazı değişiklikler yapıyoruz. Aşağıda tümü yer alan güncellenen şartları incelemenizi öneriyoruz:'
-      description_html: Bu e-postayı alıyorsunuz çünkü %{domain} adresindeki hizmet şartlarımızda bazı değişiklikler yapıyoruz. <a href="%{path}" target="_blank">Tüm güncellenen şartları buraya tıklayarak</a> incelemenizi öneririz.
+      description: 'Bu e-postayı alıyorsunuz çünkü %{domain} adresindeki hizmet şartlarımızda bazı değişiklikler yapıyoruz. Bu değişiklikler %{date} tarihinde geçerli olacak. Aşağıda tümü yer alan güncellenen şartları incelemenizi öneriyoruz:'
+      description_html: Bu e-postayı alıyorsunuz çünkü %{domain} adresindeki hizmet şartlarımızda bazı değişiklikler yapıyoruz. Bu değişiklikler <strong>%{date}</strong> tarihinde geçerli olacak. <a href="%{path}" target="_blank">Tüm güncellenen şartları</a> incelemenizi öneriyoruz.
       sign_off: "%{domain} Ekibi"
       subject: Şimdiki hizmet şartlarımıza güncellemeler
       subtitle: "%{domain} adresindeki hizmet şartları değişiyor"
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 801d32b1ab..0256773d32 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -315,6 +315,7 @@ uk:
       title: Журнал подій
       unavailable_instance: "(ім'я домену недоступне)"
     announcements:
+      back: Назад до анонсів
       destroyed_msg: Оголошення успішно видалено!
       edit:
         title: Редагувати оголошення
@@ -323,6 +324,10 @@ uk:
       new:
         create: Створити оголошення
         title: Нове оголошення
+      preview:
+        disclaimer: Оскільки користувачі не можуть відмовитися від них, сповіщення по електронній пошті повинні обмежуватися важливими оголошеннями, такими як порушення особистих даних або повідомлення про закриття серверу.
+        explanation_html: 'Електронний лист буде надіслано <strong>%{display_count} користувачам</strong>. До електронного листа буде включено такий текст:'
+        title: Попередній перегляд сповіщення
       publish: Опублікувати
       published_msg: Оголошення успішно опубліковано!
       scheduled_for: Заплановано на %{time}
@@ -1923,6 +1928,10 @@ uk:
     recovery_instructions_html: У випадку втрати доступу до вашого телефону ви можете використати один з нижчевказаних кодів відновлення, щоб повернути доступ до вашого облікового запису. <strong>Тримайте коди відновлення у безпеці</strong>, наприклад, роздрукуйте їх та зберігайте разом з іншими важливими документами.
     webauthn: Ключі безпеки
   user_mailer:
+    announcement_published:
+      description: 'Адміністратори %{domain} роблять оголошення:'
+      subject: Службове оголошення
+      title: Службове оголошення сервісу %{domain}
     appeal_approved:
       action: Налаштування облікового запису
       explanation: Оскарження попередження вашому обліковому запису %{strike_date}, яке ви надіслали %{appeal_date} було схвалено. Ваш обліковий запис знову вважається добропорядним.
@@ -1955,8 +1964,6 @@ uk:
     terms_of_service_changed:
       agreement: Далі використовуючи %{domain}, ви погоджуєтеся з цими умовами. Якщо ви не згодні з оновленими умовами, ви можете припинити свою угоду з %{domain} будь-якої миті, видаливши ваш обліковий запис.
       changelog: 'Коротко, ось що це оновлення означає для вас:'
-      description: 'Ви отримали цього електронного листа, тому що ми впроваджуємо деякі зміни в наші умови обслуговування в %{domain}. Радимо переглянути оновлені умови повністю тут:'
-      description_html: Ви отримали цього електронного листа, тому що ми впроваджуємо деякі зміни до наших умов обслуговування в %{domain}. Радимо переглянути <a href="%{path}" target="_blank">повністю оновлені умови тут</a>.
       sign_off: Команда %{domain}
       subject: Оновлення до наших умов обслуговування
       subtitle: Умови використання %{domain} змінюються
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index 33c743188d..a255973a10 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -306,6 +306,7 @@ vi:
       title: Nhật ký kiểm duyệt
       unavailable_instance: "(tên máy chủ không khả dụng)"
     announcements:
+      back: Quay về thông báo
       destroyed_msg: Xóa thông báo thành công!
       edit:
         title: Sửa thông báo
@@ -314,6 +315,10 @@ vi:
       new:
         create: Tạo thông báo
         title: Tạo thông báo mới
+      preview:
+        disclaimer: Vì người dùng không thể chọn không nhận thông báo qua email nên thông báo qua email chỉ nên giới hạn ở những thông báo quan trọng như thông báo vi phạm dữ liệu cá nhân hoặc thông báo đóng máy chủ.
+        explanation_html: 'Gửi email tới <strong>%{display_count} thành viên</strong>. Nội dung sau đây sẽ được đưa vào email:'
+        title: Xem trước thông báo sẽ gửi
       publish: Đăng
       published_msg: Truyền đi thông báo thành công!
       scheduled_for: Đã lên lịch %{time}
@@ -467,6 +472,36 @@ vi:
       new:
         title: Nhập máy chủ chặn
       no_file: Không có tập tin nào được chọn
+    fasp:
+      debug:
+        callbacks:
+          created_at: Đã tạo lúc
+          delete: Xóa
+          ip: Địa chỉ IP
+          request_body: Nội dung yêu cầu
+          title: Debug Callback
+      providers:
+        active: Hoạt động
+        base_url: URL cơ sở
+        callback: Callback
+        delete: Xóa
+        edit: Chỉnh sửa nhà cung cấp
+        finish_registration: Hoàn tất đăng ký
+        name: Tên
+        providers: Nhà cung cấp
+        public_key_fingerprint: Public key fingerprint
+        registration_requested: Yêu cầu đăng ký
+        registrations:
+          confirm: Xác nhận
+          description: Bạn đã nhận được đăng ký từ FASP. Hãy từ chối nếu bạn không phải là người khởi tạo. Nếu bạn đã khởi tạo, hãy so sánh cẩn thận tên và dấu vân tay khóa trước khi xác nhận đăng ký.
+          reject: Từ chối
+          title: Xác nhận đăng ký FASP
+        save: Lưu
+        select_capabilities: Chọn khả năng
+        sign_in: Đăng nhập
+        status: Trạng thái
+        title: Fediverse Auxiliary Service Providers
+      title: FASP
     follow_recommendations:
       description_html: "<strong>Gợi ý theo dõi giúp những người mới nhanh chóng tìm thấy những nội dung thú vị</strong>. Khi một người chưa đủ tương tác với những người khác để hình thành các đề xuất theo dõi được cá nhân hóa, thì những người này sẽ được đề xuất. Nó bao gồm những người có số lượt tương tác gần đây cao nhất và số lượng người theo dõi cao nhất cho một ngôn ngữ nhất định trong máy chủ."
       language: Theo ngôn ngữ
@@ -925,6 +960,7 @@ vi:
         chance_to_review_html: "<strong>Các điều khoản dịch vụ đã tạo sẽ không được tự động công bố.</strong> Bạn sẽ cần xem lại trước. Vui lòng điền thông tin cần thiết để tiếp tục."
         explanation_html: Mẫu điều khoản dịch vụ được cung cấp chỉ nhằm mục đích cung cấp thông tin và không được hiểu là tư vấn pháp lý về bất kỳ vấn đề nào. Vui lòng tham khảo ý kiến ​​cố vấn pháp lý của riêng bạn về tình huống của bạn và các câu hỏi pháp lý cụ thể mà bạn có.
         title: Thiết lập Điều khoản Dịch vụ
+      going_live_on_html: Chính thức áp dụng từ %{date}
       history: Lịch sử
       live: Đang hiển thị
       no_history: Hiện tại chưa có ghi nhận thay đổi nào về các điều khoản dịch vụ.
@@ -1190,6 +1226,7 @@ vi:
     too_fast: Nghi vấn đăng ký spam, xin thử lại.
     use_security_key: Dùng khóa bảo mật
     user_agreement_html: Tôi đã đọc và đồng ý với <a href="%{terms_of_service_path}" target="_blank">điều khoản dịch vụ</a> và <a href="%{privacy_policy_path}" target="_blank">chính sách bảo mật</a>
+    user_privacy_agreement_html: Tôi đã đọc và đồng ý <a href="%{privacy_policy_path}" target="_blank">chính sách bảo mật</a>
   author_attribution:
     example_title: Văn bản mẫu
     hint_html: Bạn là nhà báo hoặc blogger bên ngoài Mastodon? Kiểm soát cách bài viết của bạn được ghi nhận khi chia sẻ trên Mastodon.
@@ -1860,6 +1897,10 @@ vi:
     recovery_instructions_html: Nếu bạn bị mất điện thoại, hãy dùng một trong các mã khôi phục bên dưới để lấy lại quyền truy cập vào tài khoản của mình. <strong>Giữ mã khôi phục an toàn</strong>. Ví dụ, bạn có thể in chúng ra giấy.
     webauthn: Khóa bảo mật
   user_mailer:
+    announcement_published:
+      description: 'Quản trị viên %{domain} gửi một thông báo:'
+      subject: Thông báo dịch vụ
+      title: "%{domain} thông báo dịch vụ"
     appeal_approved:
       action: Cài đặt tài khoản
       explanation: Khiếu nại về tài khoản của bạn vào %{strike_date}, được gửi lúc %{appeal_date} đã được chấp nhận. Tài khoản của bạn đã có thể sử dụng bình thường.
@@ -1892,8 +1933,8 @@ vi:
     terms_of_service_changed:
       agreement: Tiếp tục sử dụng %{domain}, đồng nghĩa bạn đồng ý điều khoản dịch vụ. Nếu bạn không đồng ý với các điều khoản đã cập nhật, hãy xóa tài khoản %{domain} của bạn.
       changelog: 'Nhìn sơ qua, bản cập nhật này:'
-      description: 'Bạn nhận được email này vì chúng tôi đang thực hiện một số thay đổi đối với các điều khoản dịch vụ tại %{domain}. Hãy xem lại đầy đủ các điều khoản đã cập nhật tại đây:'
-      description_html: Bạn nhận được email này vì chúng tôi đang thực hiện một số thay đổi đối với các điều khoản dịch vụ tại %{domain}. Hãy xem lại <a href="%{path}" target="_blank">đầy đủ các điều khoản được cập nhật ở đây</a>.
+      description: 'Bạn nhận được email này vì chúng tôi đang thực hiện một số thay đổi đối với các điều khoản dịch vụ tại %{domain}. Thay đổi sẽ có hiệu lực từ %{date}. Hãy xem lại đầy đủ các điều khoản đã cập nhật tại đây:'
+      description_html: Bạn nhận được email này vì chúng tôi đang thực hiện một số thay đổi đối với các điều khoản dịch vụ tại %{domain}. Thay đổi sẽ có hiệu lực từ <strong>%{date}</strong>. Hãy xem lại <a href="%{path}" target="_blank">đầy đủ các điều khoản được cập nhật ở đây</a>.
       sign_off: Đội ngũ %{domain}
       subject: Cập nhật điều khoản dịch vụ
       subtitle: Điều khoản dịch vụ tại %{domain} đã thay đổi
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index a7fbfa3de2..3b87654041 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -306,6 +306,7 @@ zh-CN:
       title: 审核日志
       unavailable_instance: "(域名不可用)"
     announcements:
+      back: 回到公告
       destroyed_msg: 公告已删除!
       edit:
         title: 编辑公告
@@ -314,6 +315,10 @@ zh-CN:
       new:
         create: 创建公告
         title: 新公告
+      preview:
+        disclaimer: 由于用户无法选择退出,电子邮件通知应仅限于重要公告,例如个人数据泄露或服务器关闭通知。
+        explanation_html: 此电子邮件将发送给 <strong>%{display_count} 用户</strong>。电子邮件将包含以下文本:
+        title: 预览公告通知
       publish: 发布
       published_msg: 公告已发布!
       scheduled_for: 定时在 %{time}
@@ -467,6 +472,30 @@ zh-CN:
       new:
         title: 导入域名列表
       no_file: 没有选择文件
+    fasp:
+      debug:
+        callbacks:
+          created_at: 创建于
+          delete: 刪除
+          ip: IP 地址
+          request_body: 请求正文
+          title: 调试回调
+      providers:
+        active: 有效
+        base_url: 基础 URL
+        callback: 回调
+        delete: 刪除
+        edit: 编辑提供商
+        finish_registration: 完成注册
+        name: 名称
+        providers: 提供商
+        public_key_fingerprint: 公钥指纹
+        registrations:
+          confirm: 确认
+          reject: 拒绝
+        save: 保存
+        sign_in: 登录
+        status: 状态
     follow_recommendations:
       description_html: "<strong>“关注推荐”可帮助新用户快速找到有趣的内容</strong>。 当用户与他人的互动不足以形成个性化的建议时,就会推荐关注这些账号。推荐会每日更新,基于选定语言的近期最高互动数和最多本站关注者数综合评估得出。"
       language: 选择语言
@@ -1303,7 +1332,7 @@ zh-CN:
       request: 请求你的存档
       size: 大小
     blocks: 屏蔽的用户
-    bookmarks: 收藏
+    bookmarks: 书签
     csv: CSV
     domain_blocks: 站点屏蔽列表
     lists: 列表
@@ -1426,18 +1455,18 @@ zh-CN:
     time_started: 开始于
     titles:
       blocking: 正在导入被屏蔽的账号
-      bookmarks: 正在导入收藏
+      bookmarks: 正在导入书签
       domain_blocking: 正在导入站点屏蔽列表
       following: 正在导入关注的账号
       lists: 导入列表
       muting: 正在导入隐藏的账号
     type: 导入类型
     type_groups:
-      constructive: 关注与收藏
+      constructive: 关注和书签
       destructive: 屏蔽与隐藏
     types:
       blocking: 屏蔽列表
-      bookmarks: 收藏
+      bookmarks: 书签
       domain_blocking: 站点屏蔽列表
       following: 关注列表
       lists: 列表
@@ -1803,8 +1832,8 @@ zh-CN:
     keep_pinned_hint: 不会删除你的任何置顶嘟文
     keep_polls: 保留投票
     keep_polls_hint: 不删除你的任何投票
-    keep_self_bookmark: 保存被你加入书签的嘟文
-    keep_self_bookmark_hint: 如果你已将自己的嘟文添加书签,就不会删除这些嘟文
+    keep_self_bookmark: 保留你加入书签的嘟文
+    keep_self_bookmark_hint: 不删除书签中你自己的嘟文
     keep_self_fav: 保留你喜欢的嘟文
     keep_self_fav_hint: 如果你喜欢了自己的嘟文,则不会删除这些嘟文
     min_age:
@@ -1861,6 +1890,10 @@ zh-CN:
     recovery_instructions_html: 如果你的手机无法使用,你可以使用下列任意一个恢复代码来重新获得对账号的访问权。<strong>请妥善保管好你的恢复代码</strong>(例如,你可以将它们打印出来,然后和其他重要的文件放在一起)。
     webauthn: 安全密钥
   user_mailer:
+    announcement_published:
+      description: "%{domain}管理员发布了一则公告:"
+      subject: 服务公告
+      title: "%{domain}服务公告"
     appeal_approved:
       action: 账号设置
       explanation: 你于 %{appeal_date} 对 %{strike_date} 在你账号上做出的处罚提出的申诉已被批准,你的账号已回到正常状态。
@@ -1893,8 +1926,6 @@ zh-CN:
     terms_of_service_changed:
       agreement: 继续使用你在 %{domain} 的账号即表示您同意这些条款。如果你不同意更新后的条款,你可以随时删除账号以终止与 %{domain} 的协议。
       changelog: 本次更新的要点如下:
-      description: 你收到此邮件是因为我们更新了 %{domain} 的服务条款。我们建议你在此查看变更后的服务条款:
-      description_html: 你收到此邮件是因为我们更新了 %{domain} 的服务条款。我们建议你在此查看<a href="%{path}" target="_blank">变更后的服务条款</a>。
       sign_off: "%{domain} 运营团队"
       subject: 服务条款变更
       subtitle: "%{domain} 更新了服务条款"
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 7278058dce..43f4f3ecba 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -38,7 +38,7 @@ zh-HK:
         current_email: 現時電郵
         label: 更改電郵
         new_email: 新的電郵
-        submit: 改變電郵
+        submit: 更改電郵
         title: 改變 %{username} 的電郵
       change_role:
         changed_msg: 成功更改身份!
@@ -182,6 +182,7 @@ zh-HK:
         destroy_domain_block: 刪除已封鎖的域名
         destroy_instance: 清除網域
         destroy_ip_block: 刪除 IP 規則
+        destroy_relay: 刪除中繼
         destroy_status: 刪除文章
         destroy_unavailable_domain: 刪除無效域名
         destroy_user_role: 刪除身份
@@ -425,6 +426,12 @@ zh-HK:
       new:
         title: 匯入封鎖的網域
       no_file: 未選擇檔案
+    fasp:
+      debug:
+        callbacks:
+          delete: 刪除
+      providers:
+        delete: 刪除
     follow_recommendations:
       description_html: "<strong>跟隨建議幫助新使用者快速找到有趣內容。</strong> 當使用者尚未和其他帳號足夠多的互動以產生個人化建議時,以下帳號將被推荐。這些是一句指定語言的近期參與度和本地粉絲數最高之帳戶組合每日重新計算。"
       language: 按語言
@@ -1549,6 +1556,7 @@ zh-HK:
     import: 匯入
     import_and_export: 匯入及匯出
     migrate: 帳戶遷移
+    notifications: 電郵通知
     preferences: 偏好設定
     profile: 個人資料
     relationships: 關注及追隨者
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 4a3ab2ba23..fae08b3216 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -306,6 +306,7 @@ zh-TW:
       title: 營運日誌
       unavailable_instance: "(該域名無法使用)"
     announcements:
+      back: 回到公告
       destroyed_msg: 成功刪除公告!
       edit:
         title: 編輯公告
@@ -314,6 +315,10 @@ zh-TW:
       new:
         create: 新增公告
         title: 新增公告
+      preview:
+        disclaimer: 由於使用者無法選擇退出,電子郵件通知應僅限於重要公告,例如個人資料洩露或伺服器關閉通知。
+        explanation_html: 此 email 將寄至 <strong>%{display_count} 名使用者</strong>。以下文字將被包含於 e-mail 中:
+        title: 預覽公告通知
       publish: 發布
       published_msg: 成功發布公告!
       scheduled_for: 排定 %{time}
@@ -467,6 +472,36 @@ zh-TW:
       new:
         title: 匯入網域黑名單
       no_file: 尚未選擇檔案
+    fasp:
+      debug:
+        callbacks:
+          created_at: 建立於
+          delete: 刪除
+          ip: IP 地址
+          request_body: Request body
+          title: 除錯 callback
+      providers:
+        active: 生效
+        base_url: Base URL
+        callback: Callback
+        delete: 刪除
+        edit: 編輯提供商
+        finish_registration: 完成註冊
+        name: 名稱
+        providers: 提供商
+        public_key_fingerprint: 公鑰指紋
+        registration_requested: 已請求註冊
+        registrations:
+          confirm: 確認
+          description: 您收到 FASP的註冊。若您未發起此請求,請拒絕它。若您發起了此請求,請仔細比較名稱與金鑰指紋再確認註冊。
+          reject: 拒絕
+          title: 確認 FASP 註冊
+        save: 儲存
+        select_capabilities: 選擇功能
+        sign_in: 登入
+        status: 狀態
+        title: 聯邦宇宙輔助服務提供商 (Fediverse Auxiliary Service Providers)
+      title: FASP
     follow_recommendations:
       description_html: "<strong>跟隨建議幫助新使用者們快速找到有趣的內容</strong>。當使用者沒有與其他帳號有足夠多的互動以建立個人化跟隨建議時,這些帳號將會被推薦。這些帳號將基於某選定語言之高互動與高本地跟隨者數量帳號而每日重新更新。"
       language: 對於語言
@@ -927,6 +962,7 @@ zh-TW:
         chance_to_review_html: "<strong>所產生之服務條款將不會自動發佈。</strong> 您將能檢視所產生之結果。請填寫必要細節以繼續。"
         explanation_html: 提供之服務條款模板僅供參考資訊使用,不應將其作為任何法律建議。請依照您的具體情形與特定法律議題諮詢您的法律顧問。
         title: 設定服務條款
+      going_live_on_html: 目前條款,自 %{date} 起生效
       history: 歷史
       live: 目前版本
       no_history: 未有任何服務條款變更紀錄。
@@ -1863,6 +1899,10 @@ zh-TW:
     recovery_instructions_html: 若您的手機無法使用,您可以使用下列任意一個備用驗證碼來重新獲得帳號的存取權。<strong>請妥善保管好您的備用驗證碼</strong>(例如,可以將它們列印出來,與您的其他重要文件放在一起)。
     webauthn: 安全金鑰
   user_mailer:
+    announcement_published:
+      description: "%{domain} 之管理員正在進行公告:"
+      subject: 服務公告
+      title: "%{domain} 服務公告"
     appeal_approved:
       action: 帳號設定
       explanation: 您於 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示之申訴已獲批准。您的帳號再次享有良好的信譽。
@@ -1895,8 +1935,12 @@ zh-TW:
     terms_of_service_changed:
       agreement: 透過繼續使用 %{domain},您將同意這些條款。若您不同意此條款異動,您能隨時終止與 %{domain} 之協議並刪除您的帳號。
       changelog: 簡而言之,此次更新對您將意味著:
-      description: 您收到此 e-mail 係因我們正在更新 %{domain} 之服務條款。我們鼓勵您審視此處之服務條款更新全文:
-      description_html: 您收到此 e-mail 係因我們正在更新 %{domain} 之服務條款。我們鼓勵您審視 <a href="%{path}" target="_blank">此處之服務條款更新全文</a>。
+      description: |-
+        您收到此 e-mail 係因我們正在更新 %{domain} 之服務條款。
+        這些異動將於 %{date} 起生效。我們鼓勵您審視此處之服務條款更新全文:
+      description_html: |-
+        您收到此 e-mail 係因我們正在更新 %{domain} 之服務條款。
+        這些異動將於 <strong>%{date} 起</strong> 生效。我們鼓勵您審視 <a href="%{path}" target="_blank">此處之服務條款更新全文</a>。
       sign_off: "%{domain} 團隊"
       subject: 我們的服務條款更新
       subtitle: "%{domain} 之服務條款正在悄悄發生變化"
diff --git a/config/mastodon.yml b/config/mastodon.yml
index 2a3f642795..3f110d51cb 100644
--- a/config/mastodon.yml
+++ b/config/mastodon.yml
@@ -1,5 +1,6 @@
 ---
 shared:
+  experimental_features: <%= ENV.fetch('EXPERIMENTAL_FEATURES', nil) %>
   self_destruct_value: <%= ENV.fetch('SELF_DESTRUCT', nil) %>
   software_update_url: <%= ENV.fetch('UPDATE_CHECK_URL', 'https://kmy.blue/update-check') %>
   source:
@@ -9,3 +10,5 @@ shared:
   version:
     metadata: <%= ENV.fetch('MASTODON_VERSION_METADATA', nil) %>
     prerelease: <%= ENV.fetch('MASTODON_VERSION_PRERELEASE', nil) %>
+test:
+  experimental_features: <%= [ENV.fetch('EXPERIMENTAL_FEATURES', nil), 'testing_only'].compact.join(',') %>
diff --git a/config/navigation.rb b/config/navigation.rb
index 4857ccbc16..ac9b42c661 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -82,6 +82,7 @@ SimpleNavigation::Configuration.run do |navigation|
       s.item :announcements, safe_join([material_symbol('campaign'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) }
       s.item :custom_emojis, safe_join([material_symbol('mood'), t('admin.custom_emojis.title')]), admin_custom_emojis_path, highlights_on: %r{/admin/custom_emojis}, if: -> { current_user.can?(:manage_custom_emojis) }
       s.item :webhooks, safe_join([material_symbol('inbox'), t('admin.webhooks.title')]), admin_webhooks_path, highlights_on: %r{/admin/webhooks}, if: -> { current_user.can?(:manage_webhooks) }
+      s.item :fasp, safe_join([material_symbol('extension'), t('admin.fasp.title')]), admin_fasp_providers_path, highlights_on: %r{/admin/fasp}, if: -> { current_user.can?(:manage_federation) } if Mastodon::Feature.fasp_enabled?
       s.item :relays, safe_join([material_symbol('captive_portal'), t('admin.relays.title')]), admin_relays_path, highlights_on: %r{/admin/relays}, if: -> { !limited_federation_mode? && current_user.can?(:manage_federation) }
       s.item :friend_servers, safe_join([material_symbol('captive_portal'), t('admin.friend_servers.title')]), admin_friend_servers_path, highlights_on: %r{/admin/friend_servers}, if: -> { current_user.can?(:manage_federation) }
     end
diff --git a/config/routes.rb b/config/routes.rb
index 1f219a09e3..1f97ddaaa4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -133,6 +133,7 @@ Rails.application.routes.draw do
   constraints(username: %r{[^@/.]+}) do
     with_options to: 'accounts#show' do
       get '/@:username', as: :short_account
+      get '/@:username/featured'
       get '/@:username/with_replies', as: :short_account_with_replies
       get '/@:username/media', as: :short_account_media
       get '/@:username/tagged/:tag', as: :short_account_tag
@@ -200,6 +201,8 @@ Rails.application.routes.draw do
 
   draw(:api)
 
+  draw(:fasp)
+
   draw(:web_app)
 
   get '/web/(*any)', to: redirect('/%{any}', status: 302), as: :web, defaults: { any: '' }, format: false
@@ -208,7 +211,8 @@ Rails.application.routes.draw do
 
   get '/privacy-policy',   to: 'privacy#show', as: :privacy_policy
   get '/terms-of-service', to: 'terms_of_service#show', as: :terms_of_service
-  get '/terms',            to: redirect('/terms-of-service')
+  get '/terms-of-service/:date', to: 'terms_of_service#show', as: :terms_of_service_version
+  get '/terms', to: redirect('/terms-of-service')
 
   match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false
   match '*unmatched_route', via: :all, to: 'application#raise_not_found', format: false
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index f6a0c88048..b829d0b9d8 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -65,6 +65,10 @@ namespace :admin do
       post :publish
       post :unpublish
     end
+
+    resource :preview, only: [:show], module: :announcements
+    resource :test, only: [:create], module: :announcements
+    resource :distribution, only: [:create], module: :announcements
   end
 
   with_options to: redirect('/admin/settings/branding') do
diff --git a/config/routes/api.rb b/config/routes/api.rb
index d8193ab96d..c8233bb01d 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -88,7 +88,7 @@ namespace :api, format: false do
       end
     end
 
-    resources :media, only: [:create, :update, :show]
+    resources :media, only: [:create, :update, :show, :destroy]
     resources :blocks, only: [:index]
     resources :mutes, only: [:index]
     resources :favourites, only: [:index]
@@ -133,6 +133,8 @@ namespace :api, format: false do
         resource :translation_languages, only: [:show]
         resource :languages, only: [:show]
         resource :activity, only: [:show], controller: :activity
+
+        get '/terms_of_service/:date', to: 'terms_of_services#show'
       end
     end
 
@@ -203,6 +205,7 @@ namespace :api, format: false do
         resources :circles, only: :index
         resources :identity_proofs, only: :index
         resources :featured_tags, only: :index
+        resources :endorsements, only: :index
       end
 
       member do
@@ -216,8 +219,10 @@ namespace :api, format: false do
       end
 
       scope module: :accounts do
-        resource :pin, only: :create
-        post :unpin, to: 'pins#destroy'
+        post :pin, to: 'endorsements#create'
+        post :endorse, to: 'endorsements#create'
+        post :unpin, to: 'endorsements#destroy'
+        post :unendorse, to: 'endorsements#destroy'
         resource :note, only: :create
       end
     end
diff --git a/config/routes/fasp.rb b/config/routes/fasp.rb
new file mode 100644
index 0000000000..9d052526de
--- /dev/null
+++ b/config/routes/fasp.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+namespace :api, format: false do
+  namespace :fasp do
+    namespace :debug do
+      namespace :v0 do
+        namespace :callback do
+          resources :responses, only: [:create]
+        end
+      end
+    end
+
+    resource :registration, only: [:create]
+  end
+end
+
+namespace :admin do
+  namespace :fasp do
+    namespace :debug do
+      resources :callbacks, only: [:index, :destroy]
+    end
+
+    resources :providers, only: [:index, :show, :edit, :update, :destroy] do
+      resources :debug_calls, only: [:create]
+
+      resource :registration, only: [:new, :create]
+    end
+  end
+end
diff --git a/config/templates/terms-of-service.md b/config/templates/terms-of-service.md
index 4b11a9897c..b2e32f76b9 100644
--- a/config/templates/terms-of-service.md
+++ b/config/templates/terms-of-service.md
@@ -17,7 +17,7 @@ into these Terms. You should also read these policies before using the Instance.
 
 ## Age Requirements and Responsibility of Parents and Legal Guardians
 
-By accessing the Instance, you signify that you are at least thirteen years old
+By accessing the Instance, you signify that you are at least %{min_age} years old
 and that you meet the minimum age required by the laws in your country. If you
 are old enough to access the Instance in your country, but are not old enough to
 have the legal authority to consent to our Terms, please ask your parent or
@@ -249,7 +249,7 @@ individual basis.
 ## Choice of Law
 
 Any and all claims related to or arising out of your use of, or access to the
-Instance shall be governed by internal substantive laws of New York in all
+Instance shall be governed by internal substantive laws of %{choice_of_law} in all
 respects, without regard for the jurisdiction or forum in which you are
 domiciled, reside, or located at the time of such access or use.
 
diff --git a/config/webpack/production.js b/config/webpack/production.js
index 7f1ee4a8f9..c9c607c68e 100644
--- a/config/webpack/production.js
+++ b/config/webpack/production.js
@@ -48,7 +48,7 @@ module.exports = merge(sharedConfig, {
       logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
     }),
     new InjectManifest({
-      additionalManifestEntries: ['1f602.svg', 'sheet_13.png'].map((filename) => {
+      additionalManifestEntries: ['1f602.svg', 'sheet_15.png'].map((filename) => {
         const path = resolve(root, 'public', 'emoji', filename);
         const body = readFileSync(path);
         const md5  = createHash('md5');
diff --git a/config/webpack/rules/babel.js b/config/webpack/rules/babel.js
index f1b53c3606..76e41f3df0 100644
--- a/config/webpack/rules/babel.js
+++ b/config/webpack/rules/babel.js
@@ -4,7 +4,7 @@ const { env, settings } = require('../configuration');
 
 // Those modules contain modern ES code that need to be transpiled for Webpack to process it
 const nodeModulesToProcess = [
-  '@reduxjs', 'fuzzysort', 'toygrad'
+  '@reduxjs', 'fuzzysort', 'toygrad', '@react-spring'
 ];
 
 module.exports = {
diff --git a/config/webpacker.yml b/config/webpacker.yml
index e07f577c5e..ac4bca2916 100644
--- a/config/webpacker.yml
+++ b/config/webpacker.yml
@@ -13,8 +13,8 @@ default: &default
   # ['app/assets', 'engine/foo/app/assets']
   resolved_paths: []
 
-  # Reload manifest.json on all requests so we reload latest compiled packs
-  cache_manifest: false
+  # Cache manifest.json for performance
+  cache_manifest: true
 
   # Extract and emit a css file
   extract_css: true
@@ -55,6 +55,9 @@ development:
 
   compile: true
 
+  # Reload manifest in development environment so we pick up changes
+  cache_manifest: false
+
   # Reference: https://webpack.js.org/configuration/dev-server/
   dev_server:
     https: false
@@ -89,6 +92,3 @@ production:
 
   # Production depends on precompilation of packs prior to booting for performance.
   compile: false
-
-  # Cache manifest.json for performance
-  cache_manifest: true
diff --git a/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb b/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
index efd821f018..8bea8d5371 100644
--- a/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
+++ b/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
@@ -8,13 +8,13 @@ class ImproveIndexForPublicTimelineSpeed < ActiveRecord::Migration[7.0]
                                              where: '(local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility IN (0, 10, 11) AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
     add_index :statuses, [:id, :account_id], name: :index_statuses_public_20231213, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility IN (0, 10, 11) AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
     remove_index :statuses, name: :index_statuses_local_20190824 # rubocop:disable Naming/VariableNumber
-    remove_index :statuses, name: :index_statuses_public_20200119 # rubocop:disable Naming/VariableNumber
+    remove_index :statuses, if_exists: true, name: :index_statuses_public_20200119 # rubocop:disable Naming/VariableNumber
   end
 
   def down
     add_index :statuses, [:id, :account_id], name: :index_statuses_local_20190824, algorithm: :concurrently, order: { id: :desc }, where: '(local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
     add_index :statuses, [:id, :account_id], name: :index_statuses_public_20200119, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
     remove_index :statuses, name: :index_statuses_local_20231213 # rubocop:disable Naming/VariableNumber
-    remove_index :statuses, name: :index_statuses_public_20231213 # rubocop:disable Naming/VariableNumber
+    remove_index :statuses, if_exists: true, name: :index_statuses_public_20231213 # rubocop:disable Naming/VariableNumber
   end
 end
diff --git a/db/migrate/20240918233930_add_fetched_replies_at_to_status.rb b/db/migrate/20240918233930_add_fetched_replies_at_to_status.rb
new file mode 100644
index 0000000000..229e43d978
--- /dev/null
+++ b/db/migrate/20240918233930_add_fetched_replies_at_to_status.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddFetchedRepliesAtToStatus < ActiveRecord::Migration[7.1]
+  def change
+    add_column :statuses, :fetched_replies_at, :datetime, null: true
+  end
+end
diff --git a/db/migrate/20241205103523_create_fasp_providers.rb b/db/migrate/20241205103523_create_fasp_providers.rb
new file mode 100644
index 0000000000..ac1d52e8a7
--- /dev/null
+++ b/db/migrate/20241205103523_create_fasp_providers.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class CreateFaspProviders < ActiveRecord::Migration[7.2]
+  def change
+    create_table :fasp_providers do |t|
+      t.boolean :confirmed, null: false, default: false
+      t.string :name, null: false
+      t.string :base_url, null: false, index: { unique: true }
+      t.string :sign_in_url
+      t.string :remote_identifier, null: false
+      t.string :provider_public_key_pem, null: false
+      t.string :server_private_key_pem, null: false
+      t.jsonb :capabilities, null: false, default: []
+      t.jsonb :privacy_policy
+      t.string :contact_email
+      t.string :fediverse_account
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20241206131513_create_fasp_debug_callbacks.rb b/db/migrate/20241206131513_create_fasp_debug_callbacks.rb
new file mode 100644
index 0000000000..6b221ce93f
--- /dev/null
+++ b/db/migrate/20241206131513_create_fasp_debug_callbacks.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class CreateFaspDebugCallbacks < ActiveRecord::Migration[7.2]
+  def change
+    create_table :fasp_debug_callbacks do |t|
+      t.references :fasp_provider, null: false, foreign_key: true
+      t.string :ip, null: false
+      t.text :request_body, null: false
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20250221143646_add_notification_sent_at_to_announcements.rb b/db/migrate/20250221143646_add_notification_sent_at_to_announcements.rb
new file mode 100644
index 0000000000..9cf1521a82
--- /dev/null
+++ b/db/migrate/20250221143646_add_notification_sent_at_to_announcements.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddNotificationSentAtToAnnouncements < ActiveRecord::Migration[8.0]
+  def change
+    add_column :announcements, :notification_sent_at, :datetime
+  end
+end
diff --git a/db/migrate/20250224144617_add_effective_date_to_terms_of_services.rb b/db/migrate/20250224144617_add_effective_date_to_terms_of_services.rb
new file mode 100644
index 0000000000..e46378387d
--- /dev/null
+++ b/db/migrate/20250224144617_add_effective_date_to_terms_of_services.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddEffectiveDateToTermsOfServices < ActiveRecord::Migration[8.0]
+  def change
+    add_column :terms_of_services, :effective_date, :date
+  end
+end
diff --git a/db/migrate/20250305074104_add_effective_date_index_to_terms_of_services.rb b/db/migrate/20250305074104_add_effective_date_index_to_terms_of_services.rb
new file mode 100644
index 0000000000..78cc809ec8
--- /dev/null
+++ b/db/migrate/20250305074104_add_effective_date_index_to_terms_of_services.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddEffectiveDateIndexToTermsOfServices < ActiveRecord::Migration[8.0]
+  disable_ddl_transaction!
+
+  def change
+    add_index :terms_of_services, :effective_date, unique: true, algorithm: :concurrently, where: 'effective_date IS NOT NULL'
+  end
+end
diff --git a/db/migrate/20250313123400_add_age_verified_at_to_users.rb b/db/migrate/20250313123400_add_age_verified_at_to_users.rb
new file mode 100644
index 0000000000..c6cd6120ef
--- /dev/null
+++ b/db/migrate/20250313123400_add_age_verified_at_to_users.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddAgeVerifiedAtToUsers < ActiveRecord::Migration[8.0]
+  def change
+    add_column :users, :age_verified_at, :datetime
+  end
+end
diff --git a/db/migrate/20250410144908_drop_imports.rb b/db/migrate/20250410144908_drop_imports.rb
new file mode 100644
index 0000000000..7be9daf750
--- /dev/null
+++ b/db/migrate/20250410144908_drop_imports.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class DropImports < ActiveRecord::Migration[7.1]
+  def up
+    drop_table :imports
+  end
+
+  def down
+    raise ActiveRecord::IrreversibleMigration
+  end
+end
diff --git a/db/post_migrate/20250129144440_add_new_public_index_to_statuses.rb b/db/post_migrate/20250129144440_add_new_public_index_to_statuses.rb
new file mode 100644
index 0000000000..82cc9128b6
--- /dev/null
+++ b/db/post_migrate/20250129144440_add_new_public_index_to_statuses.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddNewPublicIndexToStatuses < ActiveRecord::Migration[8.0]
+  disable_ddl_transaction!
+
+  def change
+    add_index :statuses, [:id, :language, :account_id], name: :index_statuses_public_20250129, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
+  end
+end
diff --git a/db/post_migrate/20250129144813_remove_old_public_index_to_statuses.rb b/db/post_migrate/20250129144813_remove_old_public_index_to_statuses.rb
new file mode 100644
index 0000000000..306fde7449
--- /dev/null
+++ b/db/post_migrate/20250129144813_remove_old_public_index_to_statuses.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class RemoveOldPublicIndexToStatuses < ActiveRecord::Migration[8.0]
+  def change; end
+end
diff --git a/db/post_migrate/20250216231806_add_new_public_index_for_kmyblue_to_statuses.rb b/db/post_migrate/20250216231806_add_new_public_index_for_kmyblue_to_statuses.rb
new file mode 100644
index 0000000000..f63b5242b3
--- /dev/null
+++ b/db/post_migrate/20250216231806_add_new_public_index_for_kmyblue_to_statuses.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddNewPublicIndexForKmyblueToStatuses < ActiveRecord::Migration[8.0]
+  disable_ddl_transaction!
+
+  def change
+    add_index :statuses, [:id, :language, :account_id], name: :index_statuses_public_20250210, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility IN (0, 10, 11) AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
+  end
+end
diff --git a/db/post_migrate/20250216231904_remove_old_public_index_for_original_to_statuses.rb b/db/post_migrate/20250216231904_remove_old_public_index_for_original_to_statuses.rb
new file mode 100644
index 0000000000..2a2371b50c
--- /dev/null
+++ b/db/post_migrate/20250216231904_remove_old_public_index_for_original_to_statuses.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RemoveOldPublicIndexForOriginalToStatuses < ActiveRecord::Migration[8.0]
+  disable_ddl_transaction!
+
+  def up
+    remove_index :statuses, [:id, :account_id], if_exists: true, name: :index_statuses_public_20231213, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility IN (0, 10, 11) AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
+    remove_index :statuses, [:id, :account_id], if_exists: true, name: :index_statuses_public_20200119, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
+    remove_index :statuses, [:id, :language, :account_id], name: :index_statuses_public_20250129, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
+  end
+
+  def down
+    add_index :statuses, [:id, :language, :account_id], name: :index_statuses_public_20250129, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))' # rubocop:disable Naming/VariableNumber
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e353c6a8fb..d9a082eb24 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
+ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do
   # These are extensions that must be enabled in order to support this database
   enable_extension "pg_catalog.plpgsql"
 
@@ -265,6 +265,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
     t.datetime "updated_at", precision: nil, null: false
     t.datetime "published_at", precision: nil
     t.bigint "status_ids", array: true
+    t.datetime "notification_sent_at"
   end
 
   create_table "annual_report_statuses_per_account_counts", force: :cascade do |t|
@@ -609,6 +610,32 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
     t.index ["uri"], name: "index_emoji_reactions_on_uri", unique: true
   end
 
+  create_table "fasp_debug_callbacks", force: :cascade do |t|
+    t.bigint "fasp_provider_id", null: false
+    t.string "ip", null: false
+    t.text "request_body", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["fasp_provider_id"], name: "index_fasp_debug_callbacks_on_fasp_provider_id"
+  end
+
+  create_table "fasp_providers", force: :cascade do |t|
+    t.boolean "confirmed", default: false, null: false
+    t.string "name", null: false
+    t.string "base_url", null: false
+    t.string "sign_in_url"
+    t.string "remote_identifier", null: false
+    t.string "provider_public_key_pem", null: false
+    t.string "server_private_key_pem", null: false
+    t.jsonb "capabilities", default: [], null: false
+    t.jsonb "privacy_policy"
+    t.string "contact_email"
+    t.string "fediverse_account"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["base_url"], name: "index_fasp_providers_on_base_url", unique: true
+  end
+
   create_table "favourites", force: :cascade do |t|
     t.datetime "created_at", precision: nil, null: false
     t.datetime "updated_at", precision: nil, null: false
@@ -712,19 +739,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
     t.index ["user_id"], name: "index_identities_on_user_id"
   end
 
-  create_table "imports", force: :cascade do |t|
-    t.integer "type", null: false
-    t.boolean "approved", default: false, null: false
-    t.datetime "created_at", precision: nil, null: false
-    t.datetime "updated_at", precision: nil, null: false
-    t.string "data_file_name"
-    t.string "data_content_type"
-    t.datetime "data_updated_at", precision: nil
-    t.bigint "account_id", null: false
-    t.boolean "overwrite", default: false, null: false
-    t.integer "data_file_size"
-  end
-
   create_table "instance_infos", force: :cascade do |t|
     t.string "domain", default: "", null: false
     t.string "software", default: "", null: false
@@ -1419,13 +1433,14 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
     t.boolean "markdown", default: false
     t.integer "limited_scope"
     t.bigint "quote_of_id"
+    t.datetime "fetched_replies_at"
     t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
     t.index ["account_id", "reblog_of_id", "deleted_at", "searchability"], name: "index_statuses_for_get_following_accounts_to_search", where: "((deleted_at IS NULL) AND (reblog_of_id IS NULL) AND (searchability = ANY (ARRAY[0, 10, 1])))"
     t.index ["account_id"], name: "index_statuses_on_account_id"
     t.index ["conversation_id"], name: "index_statuses_on_conversation_id"
     t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
     t.index ["id", "account_id"], name: "index_statuses_local_20231213", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = ANY (ARRAY[0, 10, 11])) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
-    t.index ["id", "account_id"], name: "index_statuses_public_20231213", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = ANY (ARRAY[0, 10, 11])) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
+    t.index ["id", "language", "account_id"], name: "index_statuses_public_20250210", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = ANY (ARRAY[0, 10, 11])) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
     t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
     t.index ["quote_of_id", "account_id"], name: "index_statuses_on_quote_of_id_and_account_id"
@@ -1481,6 +1496,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
     t.datetime "notification_sent_at"
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
+    t.date "effective_date"
+    t.index ["effective_date"], name: "index_terms_of_services_on_effective_date", unique: true, where: "(effective_date IS NOT NULL)"
   end
 
   create_table "tombstones", force: :cascade do |t|
@@ -1555,6 +1572,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
     t.text "settings"
     t.string "time_zone"
     t.string "otp_secret"
+    t.datetime "age_verified_at"
     t.index ["account_id"], name: "index_users_on_account_id"
     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
     t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"
@@ -1675,6 +1693,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
   add_foreign_key "emoji_reactions", "accounts", on_delete: :cascade
   add_foreign_key "emoji_reactions", "custom_emojis", on_delete: :cascade
   add_foreign_key "emoji_reactions", "statuses", on_delete: :cascade
+  add_foreign_key "fasp_debug_callbacks", "fasp_providers"
   add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade
   add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
   add_foreign_key "featured_tags", "accounts", on_delete: :cascade
@@ -1688,7 +1707,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_30_232529) do
   add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
   add_foreign_key "generated_annual_reports", "accounts"
   add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
-  add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
   add_foreign_key "invites", "users", on_delete: :cascade
   add_foreign_key "list_accounts", "accounts", on_delete: :cascade
   add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
diff --git a/docker-compose.yml b/docker-compose.yml
index 47eb36698f..0ec0c43bb6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -59,7 +59,7 @@ services:
   web:
     # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
     build: .
-    image: kmyblue:17.0
+    image: kmyblue:18.0-dev
     restart: always
     env_file: .env.production
     command: bundle exec puma -C config/puma.rb
@@ -83,7 +83,7 @@ services:
     build:
       dockerfile: ./streaming/Dockerfile
       context: .
-    image: kmyblue-streaming:17.0
+    image: kmyblue-streaming:18.0-dev
     restart: always
     env_file: .env.production
     command: node ./streaming/index.js
@@ -101,7 +101,7 @@ services:
 
   sidekiq:
     build: .
-    image: kmyblue:17.0
+    image: kmyblue:18.0-dev
     restart: always
     env_file: .env.production
     command: bundle exec sidekiq
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000000..99279b56d7
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,395 @@
+// @ts-check
+
+import js from '@eslint/js';
+import { globalIgnores } from 'eslint/config';
+import formatjs from 'eslint-plugin-formatjs';
+// @ts-expect-error -- No typings
+import importPlugin from 'eslint-plugin-import';
+import jsdoc from 'eslint-plugin-jsdoc';
+import jsxA11Y from 'eslint-plugin-jsx-a11y';
+import promisePlugin from 'eslint-plugin-promise';
+import react from 'eslint-plugin-react';
+import reactHooks from 'eslint-plugin-react-hooks';
+import globals from 'globals';
+import tseslint from 'typescript-eslint';
+
+/** @type {import('typescript-eslint').ConfigArray} */
+export const baseConfig = [
+  js.configs.recommended,
+  importPlugin.flatConfigs.recommended,
+  jsdoc.configs['flat/recommended'],
+  promisePlugin.configs['flat/recommended'],
+  {
+    linterOptions: {
+      reportUnusedDisableDirectives: 'error',
+      reportUnusedInlineConfigs: 'error',
+    },
+    rules: {
+      'consistent-return': 'error',
+      'dot-notation': 'error',
+
+      eqeqeq: [
+        'error',
+        'always',
+        {
+          null: 'ignore',
+        },
+      ],
+
+      'no-console': [
+        'warn',
+        {
+          allow: ['error', 'warn'],
+        },
+      ],
+
+      'no-empty': [
+        'error',
+        {
+          allowEmptyCatch: true,
+        },
+      ],
+
+      'no-restricted-properties': [
+        'error',
+        {
+          property: 'substring',
+          message: 'Use .slice instead of .substring.',
+        },
+        {
+          property: 'substr',
+          message: 'Use .slice instead of .substr.',
+        },
+      ],
+
+      'no-unused-expressions': 'error',
+      'no-unused-vars': 'off',
+
+      'valid-typeof': 'error',
+
+      'import/extensions': [
+        'error',
+        'always',
+        {
+          js: 'never',
+          jsx: 'never',
+          mjs: 'never',
+          ts: 'never',
+          mts: 'never',
+          tsx: 'never',
+        },
+      ],
+      'import/first': 'error',
+      'import/newline-after-import': 'error',
+      'import/no-anonymous-default-export': 'error',
+      'import/no-amd': 'error',
+      'import/no-commonjs': 'error',
+      'import/no-import-module-exports': 'error',
+      'import/no-relative-packages': 'error',
+      'import/no-self-import': 'error',
+      'import/no-useless-path-segments': 'error',
+      'import/order': [
+        'error',
+        {
+          alphabetize: {
+            order: 'asc',
+          },
+
+          'newlines-between': 'always',
+
+          groups: [
+            'builtin',
+            'external',
+            'internal',
+            'parent',
+            ['index', 'sibling'],
+            'object',
+          ],
+
+          pathGroups: [
+            {
+              pattern: '{react,react-dom,react-dom/client,prop-types}',
+              group: 'builtin',
+              position: 'after',
+            },
+            {
+              pattern: '{react-intl,intl-messageformat}',
+              group: 'builtin',
+              position: 'after',
+            },
+            {
+              pattern:
+                '{classnames,react-helmet,react-router,react-router-dom}',
+              group: 'external',
+              position: 'before',
+            },
+            {
+              pattern:
+                '{immutable,@reduxjs/toolkit,react-redux,react-immutable-proptypes,react-immutable-pure-component}',
+              group: 'external',
+              position: 'before',
+            },
+            {
+              pattern: '{mastodon/**}',
+              group: 'internal',
+              position: 'after',
+            },
+          ],
+
+          pathGroupsExcludedImportTypes: [],
+        },
+      ],
+
+      'jsdoc/check-types': 'off',
+      'jsdoc/no-undefined-types': 'off',
+      'jsdoc/require-jsdoc': 'off',
+      'jsdoc/require-param-description': 'off',
+      'jsdoc/require-property-description': 'off',
+      'jsdoc/require-returns-description': 'off',
+      'jsdoc/require-returns': 'off',
+
+      'promise/always-return': 'off',
+      'promise/catch-or-return': [
+        'error',
+        {
+          allowFinally: true,
+        },
+      ],
+      'promise/no-callback-in-promise': 'off',
+      'promise/no-nesting': 'off',
+      'promise/no-promise-in-callback': 'off',
+    },
+  },
+];
+
+export default tseslint.config([
+  baseConfig,
+  globalIgnores([
+    'build/**/*',
+    'coverage/**/*',
+    'db/**/*',
+    'lib/**/*',
+    'log/**/*',
+    'node_modules/**/*',
+    'public/**/*',
+    '!public/embed.js',
+    'spec/**/*',
+    'tmp/**/*',
+    'vendor/**/*',
+    'streaming/**/*',
+  ]),
+  react.configs.flat.recommended,
+  react.configs.flat['jsx-runtime'],
+  reactHooks.configs['recommended-latest'],
+  jsxA11Y.flatConfigs.recommended,
+  importPlugin.flatConfigs.react,
+  // @ts-expect-error -- For some reason the formatjs package exports an empty object?
+  formatjs.configs.strict,
+  {
+    languageOptions: {
+      globals: {
+        ...globals.browser,
+      },
+
+      parser: tseslint.parser,
+      ecmaVersion: 2021,
+      sourceType: 'module',
+    },
+
+    settings: {
+      react: {
+        version: 'detect',
+      },
+
+      'import/ignore': ['node_modules', '\\.(css|scss|json)$'],
+
+      'import/resolver': {
+        typescript: {},
+      },
+    },
+
+    rules: {
+      'no-restricted-syntax': [
+        'error',
+        {
+          // eslint-disable-next-line no-restricted-syntax
+          selector: 'Literal[value=/•/], JSXText[value=/•/]',
+          // eslint-disable-next-line no-restricted-syntax
+          message: "Use '·' (middle dot) instead of '•' (bullet)",
+        },
+      ],
+
+      'formatjs/enforce-description': 'off', // description values not currently used
+      'formatjs/enforce-id': 'off', // Explicit IDs are used in the project
+      'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx
+      'formatjs/no-invalid-icu': 'error',
+      'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings
+      'formatjs/no-multiple-plurals': 'off', // Should be looked at
+
+      'jsx-a11y/click-events-have-key-events': 'off',
+      'jsx-a11y/label-has-associated-control': 'off',
+      'jsx-a11y/media-has-caption': 'off',
+      'jsx-a11y/no-autofocus': 'off',
+      'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off',
+      'jsx-a11y/no-noninteractive-tabindex': 'off',
+      'jsx-a11y/no-static-element-interactions': [
+        'warn',
+        {
+          handlers: ['onClick'],
+        },
+      ],
+
+      'import/no-extraneous-dependencies': [
+        'error',
+        {
+          devDependencies: [
+            'eslint.config.mjs',
+            'config/webpack/**',
+            'app/javascript/mastodon/performance.js',
+            'app/javascript/mastodon/test_setup.js',
+            'app/javascript/**/__tests__/**',
+          ],
+        },
+      ],
+      'import/no-webpack-loader-syntax': 'error',
+
+      'react/jsx-filename-extension': [
+        'error',
+        {
+          extensions: ['.jsx', 'tsx'],
+        },
+      ],
+
+      'react/jsx-boolean-value': 'error',
+      'react/display-name': 'off',
+      'react/jsx-fragments': ['error', 'syntax'],
+      'react/jsx-equals-spacing': 'error',
+      'react/jsx-no-bind': 'error',
+      'react/jsx-no-useless-fragment': 'error',
+      'react/jsx-no-target-blank': [
+        'error',
+        {
+          allowReferrer: true,
+        },
+      ],
+      'react/jsx-tag-spacing': 'error',
+      'react/jsx-wrap-multilines': 'error',
+      'react/self-closing-comp': 'error',
+    },
+  },
+  {
+    files: [
+      'app/javascript/mastodon/common.js',
+      'app/javascript/mastodon/features/emoji/unicode_to_unified_name.js',
+      'app/javascript/mastodon/features/emoji/emoji_compressed.js',
+      'app/javascript/mastodon/features/emoji/unicode_to_filename.js',
+      'app/javascript/mastodon/service_worker/web_push_locales.js',
+      '**/*.config.js',
+      '**/.*rc.js',
+      '**/ide-helper.js',
+      'config/webpack/**/*',
+      'config/formatjs-formatter.js',
+    ],
+
+    languageOptions: {
+      globals: {
+        ...globals.commonjs,
+        ...globals.node,
+      },
+
+      ecmaVersion: 5,
+      sourceType: 'commonjs',
+    },
+
+    rules: {
+      'import/no-commonjs': 'off',
+    },
+  },
+  {
+    files: ['**/*.ts', '**/*.tsx'],
+
+    extends: [
+      tseslint.configs.strictTypeChecked,
+      tseslint.configs.stylisticTypeChecked,
+      react.configs.flat.recommended,
+      react.configs.flat['jsx-runtime'],
+      reactHooks.configs['recommended-latest'],
+      jsxA11Y.flatConfigs.recommended,
+      importPlugin.flatConfigs.react,
+      importPlugin.flatConfigs.typescript,
+      jsdoc.configs['flat/recommended-typescript'],
+    ],
+
+    languageOptions: {
+      parserOptions: {
+        projectService: true,
+      },
+    },
+
+    rules: {
+      // This is not needed as we use noImplicitReturns, which handles this in addition to understanding types
+      'consistent-return': 'off',
+
+      'formatjs/enforce-plural-rules': 'off',
+
+      'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
+      'import/no-default-export': 'warn',
+
+      'jsdoc/require-jsdoc': 'off',
+
+      'react/prefer-stateless-function': 'warn',
+      'react/function-component-definition': [
+        'error',
+        {
+          namedComponents: 'arrow-function',
+        },
+      ],
+      'react/prop-types': 'off',
+
+      '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
+      '@typescript-eslint/consistent-type-exports': 'error',
+      '@typescript-eslint/consistent-type-imports': 'error',
+      '@typescript-eslint/prefer-nullish-coalescing': [
+        'error',
+        {
+          ignorePrimitives: {
+            boolean: true,
+          },
+        },
+      ],
+      '@typescript-eslint/no-restricted-imports': [
+        'warn',
+        {
+          name: 'react-redux',
+          importNames: ['useSelector', 'useDispatch'],
+          message:
+            'Use typed hooks `useAppDispatch` and `useAppSelector` instead.',
+        },
+      ],
+      '@typescript-eslint/no-unused-vars': [
+        'error',
+        {
+          vars: 'all',
+          args: 'after-used',
+          destructuredArrayIgnorePattern: '^_',
+          ignoreRestSiblings: true,
+        },
+      ],
+      '@typescript-eslint/restrict-template-expressions': [
+        'warn',
+        {
+          allowNumber: true,
+        },
+      ],
+    },
+  },
+  {
+    files: ['**/__tests__/*.js', '**/__tests__/*.jsx'],
+
+    languageOptions: {
+      globals: {
+        ...globals.jest,
+      },
+    },
+  },
+]);
diff --git a/lib/active_record/database_tasks_extensions.rb b/lib/active_record/database_tasks_extensions.rb
index d52186113c..b95421a462 100644
--- a/lib/active_record/database_tasks_extensions.rb
+++ b/lib/active_record/database_tasks_extensions.rb
@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
+require_relative '../mastodon/database'
 require_relative '../mastodon/snowflake'
 
 module ActiveRecord
@@ -8,6 +9,8 @@ module ActiveRecord
       original_load_schema = instance_method(:load_schema)
 
       define_method(:load_schema) do |db_config, *args|
+        Mastodon::Database.add_post_migrate_path_to_rails(force: true)
+
         ActiveRecord::Base.establish_connection(db_config)
         Mastodon::Snowflake.define_timestamp_id
 
diff --git a/lib/exceptions.rb b/lib/exceptions.rb
index 135f8126a4..93fcc38dce 100644
--- a/lib/exceptions.rb
+++ b/lib/exceptions.rb
@@ -12,6 +12,7 @@ module Mastodon
   class RateLimitExceededError < Error; end
   class SyntaxError < Error; end
   class InvalidParameterError < Error; end
+  class SignatureVerificationError < Error; end
 
   class UnexpectedResponseError < Error
     attr_reader :response
diff --git a/lib/mastodon/cli/feeds.rb b/lib/mastodon/cli/feeds.rb
index c0635bbcde..f594a750ed 100644
--- a/lib/mastodon/cli/feeds.rb
+++ b/lib/mastodon/cli/feeds.rb
@@ -11,17 +11,21 @@ module Mastodon::CLI
     option :concurrency, type: :numeric, default: 5, aliases: [:c]
     option :verbose, type: :boolean, aliases: [:v]
     option :dry_run, type: :boolean, default: false
+    option :skip_filled_timelines
     desc 'build [USERNAME]', 'Build home and list feeds for one or all users'
     long_desc <<-LONG_DESC
       Build home and list feeds that are stored in Redis from the database.
 
+      With the --skip-filled-timelines, timelines which contain more than half
+      the maximum number of posts will be skipped.
+
       With the --all option, all active users will be processed.
       Otherwise, a single user specified by USERNAME.
     LONG_DESC
     def build(username = nil)
       if options[:all] || username.nil?
         processed, = parallelize_with_progress(active_user_accounts) do |account|
-          PrecomputeFeedService.new.call(account) unless dry_run?
+          PrecomputeFeedService.new.call(account, skip_filled_timelines: options[:skip_filled_timelines]) unless dry_run?
         end
 
         say("Regenerated feeds for #{processed} accounts #{dry_run_mode_suffix}", :green, true)
@@ -30,7 +34,7 @@ module Mastodon::CLI
 
         fail_with_message 'No such account' if account.nil?
 
-        PrecomputeFeedService.new.call(account) unless dry_run?
+        PrecomputeFeedService.new.call(account, skip_filled_timelines: options[:skip_filled_timelines]) unless dry_run?
 
         say("OK #{dry_run_mode_suffix}", :green, true)
       else
diff --git a/lib/mastodon/cli/media.rb b/lib/mastodon/cli/media.rb
index 68420fb15d..1059eb6066 100644
--- a/lib/mastodon/cli/media.rb
+++ b/lib/mastodon/cli/media.rb
@@ -289,6 +289,15 @@ module Mastodon::CLI
       fail_with_message 'Invalid URL'
     end
 
+    PRELOADED_MODELS = %w(
+      Account
+      Backup
+      CustomEmoji
+      MediaAttachment
+      PreviewCard
+      SiteUpload
+    ).freeze
+
     private
 
     def object_storage_summary
@@ -299,7 +308,6 @@ module Mastodon::CLI
         [:headers, Account.sum(:header_file_size), Account.local.sum(:header_file_size)],
         [:preview_cards, PreviewCard.sum(:image_file_size), nil],
         [:backups, Backup.sum(:dump_file_size), nil],
-        [:imports, Import.sum(:data_file_size), nil],
         [:settings, SiteUpload.sum(:file_file_size), nil],
       ].map { |label, total, local| [label.to_s.titleize, number_to_human_size(total), local.present? ? number_to_human_size(local) : nil] }
     end
@@ -310,16 +318,6 @@ module Mastodon::CLI
       SQL
     end
 
-    PRELOADED_MODELS = %w(
-      Account
-      Backup
-      CustomEmoji
-      Import
-      MediaAttachment
-      PreviewCard
-      SiteUpload
-    ).freeze
-
     def preload_records_from_mixed_objects(objects)
       preload_map = Hash.new { |hash, key| hash[key] = [] }
 
diff --git a/lib/mastodon/database.rb b/lib/mastodon/database.rb
new file mode 100644
index 0000000000..133e45d7ac
--- /dev/null
+++ b/lib/mastodon/database.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+# This file is entirely lifted from GitLab.
+
+# The original file:
+# https://gitlab.com/gitlab-org/gitlab/-/blob/69127d59467185cf4ff1109d88ceec1f499f354f/lib/gitlab/database.rb#L244-258
+
+# Copyright (c) 2011-present GitLab B.V.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+module Mastodon
+  module Database
+    def self.add_post_migrate_path_to_rails(force: false)
+      return if ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] && !force
+
+      Rails.application.config.paths['db'].each do |db_path|
+        path = Rails.root.join(db_path, 'post_migrate').to_s
+
+        next if Rails.application.config.paths['db/migrate'].include?(path)
+
+        Rails.application.config.paths['db/migrate'] << path
+
+        # Rails memoizes migrations at certain points where it won't read the above
+        # path just yet. As such we must also update the following list of paths.
+        ActiveRecord::Migrator.migrations_paths << path
+      end
+    end
+  end
+end
diff --git a/lib/mastodon/feature.rb b/lib/mastodon/feature.rb
new file mode 100644
index 0000000000..650f1d7b8c
--- /dev/null
+++ b/lib/mastodon/feature.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Mastodon::Feature
+  class << self
+    def enabled_features
+      @enabled_features ||=
+        (Rails.configuration.x.mastodon.experimental_features || '').split(',').map(&:strip)
+    end
+
+    def method_missing(name)
+      if respond_to_missing?(name)
+        feature = name.to_s.delete_suffix('_enabled?')
+        enabled = enabled_features.include?(feature)
+        define_singleton_method(name) { enabled }
+
+        return enabled
+      end
+
+      super
+    end
+
+    def respond_to_missing?(name, include_all = false)
+      name.to_s.end_with?('_enabled?') || super
+    end
+  end
+end
diff --git a/lib/mastodon/middleware/prometheus_queue_time.rb b/lib/mastodon/middleware/prometheus_queue_time.rb
new file mode 100644
index 0000000000..bb8add51ec
--- /dev/null
+++ b/lib/mastodon/middleware/prometheus_queue_time.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Mastodon
+  module Middleware
+    class PrometheusQueueTime < ::PrometheusExporter::Middleware
+      # Overwrite to only collect the queue time metric
+      def call(env)
+        queue_time = measure_queue_time(env)
+
+        result = @app.call(env)
+
+        result
+      ensure
+        obj = {
+          type: 'web',
+          queue_time: queue_time,
+          default_labels: {},
+        }
+
+        @client.send_json(obj)
+      end
+    end
+  end
+end
diff --git a/lib/mastodon/middleware/public_file_server.rb b/lib/mastodon/middleware/public_file_server.rb
new file mode 100644
index 0000000000..b9a1edb9f5
--- /dev/null
+++ b/lib/mastodon/middleware/public_file_server.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'action_dispatch/middleware/static'
+
+module Mastodon
+  module Middleware
+    class PublicFileServer
+      SERVICE_WORKER_TTL = 7.days.to_i
+      CACHE_TTL          = 28.days.to_i
+
+      def initialize(app)
+        @app = app
+        @file_handler = ActionDispatch::FileHandler.new(Rails.application.paths['public'].first)
+      end
+
+      def call(env)
+        file = @file_handler.attempt(env)
+
+        # If the request is not a static file, move on!
+        return @app.call(env) if file.nil?
+
+        status, headers, response = file
+
+        # Set cache headers on static files. Some paths require different cache headers
+        headers['Cache-Control'] = begin
+          request_path = env['REQUEST_PATH']
+
+          if request_path.start_with?('/sw.js')
+            "public, max-age=#{SERVICE_WORKER_TTL}, must-revalidate"
+          elsif request_path.start_with?(paperclip_root_url)
+            "public, max-age=#{CACHE_TTL}, immutable"
+          else
+            "public, max-age=#{CACHE_TTL}, must-revalidate"
+          end
+        end
+
+        # Override the default CSP header set by the CSP middleware
+        headers['Content-Security-Policy'] = "default-src 'none'; form-action 'none'" if request_path.start_with?(paperclip_root_url)
+
+        headers['X-Content-Type-Options'] = 'nosniff'
+
+        [status, headers, response]
+      end
+
+      private
+
+      def paperclip_root_url
+        ENV.fetch('PAPERCLIP_ROOT_URL', '/system')
+      end
+    end
+  end
+end
diff --git a/lib/mastodon/middleware/socket_cleanup.rb b/lib/mastodon/middleware/socket_cleanup.rb
new file mode 100644
index 0000000000..8b33cb0cec
--- /dev/null
+++ b/lib/mastodon/middleware/socket_cleanup.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Mastodon
+  module Middleware
+    class SocketCleanup
+      def initialize(app)
+        @app = app
+      end
+
+      def call(env)
+        @app.call(env)
+      ensure
+        clean_up_sockets!
+      end
+
+      private
+
+      def clean_up_sockets!
+        clean_up_redis_socket!
+        clean_up_statsd_socket!
+      end
+
+      def clean_up_redis_socket!
+        RedisConnection.pool.checkin if Thread.current[:redis]
+        Thread.current[:redis] = nil
+      end
+
+      def clean_up_statsd_socket!
+        Thread.current[:statsd_socket]&.close
+        Thread.current[:statsd_socket] = nil
+      end
+    end
+  end
+end
diff --git a/lib/mastodon/rack_middleware.rb b/lib/mastodon/rack_middleware.rb
deleted file mode 100644
index 0e452f06d6..0000000000
--- a/lib/mastodon/rack_middleware.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-class Mastodon::RackMiddleware
-  def initialize(app)
-    @app = app
-  end
-
-  def call(env)
-    @app.call(env)
-  ensure
-    clean_up_sockets!
-  end
-
-  private
-
-  def clean_up_sockets!
-    clean_up_redis_socket!
-    clean_up_statsd_socket!
-  end
-
-  def clean_up_redis_socket!
-    RedisConnection.pool.checkin if Thread.current[:redis]
-    Thread.current[:redis] = nil
-  end
-
-  def clean_up_statsd_socket!
-    Thread.current[:statsd_socket]&.close
-    Thread.current[:statsd_socket] = nil
-  end
-end
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index f45b4a4305..acf7a4e79a 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -9,7 +9,7 @@ module Mastodon
     # If you change the version number, also change the image version in docker-compose.yml.
 
     def kmyblue_major
-      17
+      18
     end
 
     def kmyblue_minor
@@ -18,8 +18,8 @@ module Mastodon
 
     def kmyblue_flag
       # 'LTS'
-      # 'dev'
-      nil
+      'dev'
+      # nil
     end
 
     def major
@@ -35,7 +35,7 @@ module Mastodon
     end
 
     def default_prerelease
-      'alpha.2'
+      'alpha.4'
     end
 
     def prerelease
@@ -96,7 +96,7 @@ module Mastodon
 
     def api_versions
       {
-        mastodon: 3,
+        mastodon: 5,
         kmyblue: KMYBLUE_API_VERSION,
       }
     end
diff --git a/lib/paperclip/vips_lazy_thumbnail.rb b/lib/paperclip/vips_lazy_thumbnail.rb
index fea4b86064..528d5604dc 100644
--- a/lib/paperclip/vips_lazy_thumbnail.rb
+++ b/lib/paperclip/vips_lazy_thumbnail.rb
@@ -123,7 +123,14 @@ module Paperclip
     end
 
     def needs_convert?
-      needs_different_geometry? || needs_different_format? || needs_metadata_stripping?
+      strip_animations? || needs_different_geometry? || needs_different_format? || needs_metadata_stripping?
+    end
+
+    def strip_animations?
+      # Detecting whether the source image is animated across all our supported
+      # input file formats is not trivial, and converting unconditionally is just
+      # as simple for now
+      options[:style] == :static
     end
 
     def needs_different_geometry?
diff --git a/lib/public_file_server_middleware.rb b/lib/public_file_server_middleware.rb
deleted file mode 100644
index 7e02e37a08..0000000000
--- a/lib/public_file_server_middleware.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require 'action_dispatch/middleware/static'
-
-class PublicFileServerMiddleware
-  SERVICE_WORKER_TTL = 7.days.to_i
-  CACHE_TTL          = 28.days.to_i
-
-  def initialize(app)
-    @app = app
-    @file_handler = ActionDispatch::FileHandler.new(Rails.application.paths['public'].first)
-  end
-
-  def call(env)
-    file = @file_handler.attempt(env)
-
-    # If the request is not a static file, move on!
-    return @app.call(env) if file.nil?
-
-    status, headers, response = file
-
-    # Set cache headers on static files. Some paths require different cache headers
-    headers['Cache-Control'] = begin
-      request_path = env['REQUEST_PATH']
-
-      if request_path.start_with?('/sw.js')
-        "public, max-age=#{SERVICE_WORKER_TTL}, must-revalidate"
-      elsif request_path.start_with?(paperclip_root_url)
-        "public, max-age=#{CACHE_TTL}, immutable"
-      else
-        "public, max-age=#{CACHE_TTL}, must-revalidate"
-      end
-    end
-
-    # Override the default CSP header set by the CSP middleware
-    headers['Content-Security-Policy'] = "default-src 'none'; form-action 'none'" if request_path.start_with?(paperclip_root_url)
-
-    headers['X-Content-Type-Options'] = 'nosniff'
-
-    [status, headers, response]
-  end
-
-  private
-
-  def paperclip_root_url
-    ENV.fetch('PAPERCLIP_ROOT_URL', '/system')
-  end
-end
diff --git a/lib/redis/namespace_extensions.rb b/lib/redis/namespace_extensions.rb
index 9af59c296e..2be738b04d 100644
--- a/lib/redis/namespace_extensions.rb
+++ b/lib/redis/namespace_extensions.rb
@@ -5,6 +5,10 @@ class Redis
     def exists?(...)
       call_with_namespace('exists?', ...)
     end
+
+    def with
+      yield self
+    end
   end
 end
 
diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb
index 7815d9ed52..410a58b07e 100644
--- a/lib/sanitize_ext/sanitize_config.rb
+++ b/lib/sanitize_ext/sanitize_config.rb
@@ -155,18 +155,16 @@ class Sanitize
     )
 
     MASTODON_OEMBED = freeze_config(
-      elements: %w(audio embed iframe source video),
+      elements: %w(audio iframe source video),
 
       attributes: {
         'audio' => %w(controls),
-        'embed' => %w(height src type width),
         'iframe' => %w(allowfullscreen frameborder height scrolling src width),
         'source' => %w(src type),
         'video' => %w(controls height loop width),
       },
 
       protocols: {
-        'embed' => { 'src' => HTTP_PROTOCOLS },
         'iframe' => { 'src' => HTTP_PROTOCOLS },
         'source' => { 'src' => HTTP_PROTOCOLS },
       },
diff --git a/lib/tasks/dangerous.rake b/lib/tasks/dangerous.rake
index 62d000166b..fbd9268271 100644
--- a/lib/tasks/dangerous.rake
+++ b/lib/tasks/dangerous.rake
@@ -14,6 +14,8 @@ namespace :dangerous do
     end
 
     target_migrations = %w(
+      20250216231904
+      20250216231806
       20250130232529
       20250123091137
       20241208232829
@@ -200,9 +202,9 @@ namespace :dangerous do
     ActiveRecord::Base.connection.execute('UPDATE custom_filters SET action = 0 WHERE action = 2')
     ActiveRecord::Base.connection.execute('UPDATE account_warnings SET action = 1250 WHERE action = 1200')
     ActiveRecord::Base.connection.execute('CREATE INDEX IF NOT EXISTS index_statuses_local_20190824 ON statuses USING btree (id DESC, account_id) WHERE (local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))')
-    ActiveRecord::Base.connection.execute('CREATE INDEX IF NOT EXISTS index_statuses_public_20200119 ON statuses USING btree (id DESC, account_id) WHERE deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))')
+    ActiveRecord::Base.connection.execute('CREATE INDEX IF NOT EXISTS index_statuses_public_20250129 ON statuses USING btree (id DESC, language, account_id) WHERE deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))')
     ActiveRecord::Base.connection.execute('DROP INDEX IF EXISTS index_statuses_local_20231213')
-    ActiveRecord::Base.connection.execute('DROP INDEX IF EXISTS index_statuses_public_20231213')
+    ActiveRecord::Base.connection.execute('DROP INDEX IF EXISTS index_statuses_public_20250210')
     ActiveRecord::Base.connection.execute('ALTER TABLE ONLY custom_filter_keywords ALTER COLUMN whole_word SET DEFAULT true')
     prompt.ok 'Proceed'
 
diff --git a/lib/tasks/emojis.rake b/lib/tasks/emojis.rake
index fb18f21cf5..3b2cf46a13 100644
--- a/lib/tasks/emojis.rake
+++ b/lib/tasks/emojis.rake
@@ -103,4 +103,36 @@ namespace :emojis do
       gen_border map[emoji], 'white'
     end
   end
+
+  desc 'Download the JSON sheet data of emojis'
+  task :download_sheet_json do
+    source = 'https://raw.githubusercontent.com/iamcal/emoji-data/refs/tags/v15.1.2/emoji.json'
+    dest   = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_sheet.json')
+
+    puts "Downloading emoji data from source... (#{source})"
+
+    res = HTTP.get(source).to_s
+    data = JSON.parse(res)
+
+    filtered_data = data.map do |emoji|
+      filtered_item = {
+        'unified' => emoji['unified'],
+        'sheet_x' => emoji['sheet_x'],
+        'sheet_y' => emoji['sheet_y'],
+        'skin_variations' => {},
+      }
+
+      emoji['skin_variations']&.each do |key, variation|
+        filtered_item['skin_variations'][key] = {
+          'unified' => variation['unified'],
+          'sheet_x' => variation['sheet_x'],
+          'sheet_y' => variation['sheet_y'],
+        }
+      end
+
+      filtered_item
+    end
+
+    File.write(dest, JSON.generate(filtered_data))
+  end
 end
diff --git a/lint-staged.config.js b/lint-staged.config.js
index baf5d0d454..63f5258a94 100644
--- a/lint-staged.config.js
+++ b/lint-staged.config.js
@@ -3,7 +3,7 @@ const config = {
   'Gemfile|*.{rb,ruby,ru,rake}': 'bin/rubocop --force-exclusion -a',
   '*.{js,jsx,ts,tsx}': 'eslint --fix',
   '*.{css,scss}': 'stylelint --fix',
-  '*.haml': 'bin/haml-lint -a --parallel',
+  '*.haml': 'bin/haml-lint -a',
   '**/*.ts?(x)': () => 'tsc -p tsconfig.json --noEmit',
 };
 
diff --git a/package.json b/package.json
index 5e117a5d99..b111350da0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@mastodon/mastodon",
   "license": "AGPL-3.0-or-later",
-  "packageManager": "yarn@4.6.0",
+  "packageManager": "yarn@4.9.1",
   "engines": {
     "node": ">=18"
   },
@@ -16,14 +16,14 @@
     "clean:kbdraft": "git branch | grep kb-draft- | xargs git branch -d",
     "clean:kbtopic": "git branch | grep kbtopic- | xargs git branch -d",
     "clean:upstream": "git branch | grep upstream- | xargs git branch -d",
-    "fix:js": "eslint . --ext=.js,.jsx,.ts,.tsx --cache --report-unused-disable-directives --fix",
+    "fix:js": "eslint . --cache --fix",
     "fix:css": "stylelint --fix \"**/*.{css,scss}\"",
     "fix": "yarn fix:js && yarn fix:css",
     "format": "prettier --write --log-level warn .",
     "format:check": "prettier --check --ignore-unknown .",
     "i18n:extract": "formatjs extract 'app/javascript/**/*.{js,jsx,ts,tsx}' --ignore '**/*.d.ts' --out-file app/javascript/mastodon/locales/en.json --format config/formatjs-formatter.js",
     "jest": "cross-env NODE_ENV=test jest",
-    "lint:js": "eslint . --ext=.js,.jsx,.ts,.tsx --cache --report-unused-disable-directives",
+    "lint:js": "cd $INIT_CWD && eslint --cache --report-unused-disable-directives",
     "lint:css": "stylelint \"**/*.{css,scss}\"",
     "lint": "yarn lint:js && yarn lint:css",
     "postversion": "git push --tags",
@@ -50,18 +50,21 @@
     "@dnd-kit/core": "^6.1.0",
     "@dnd-kit/sortable": "^10.0.0",
     "@dnd-kit/utilities": "^3.2.2",
-    "@formatjs/intl-pluralrules": "^5.2.2",
+    "@emoji-mart/data": "1.2.1",
+    "@formatjs/intl-pluralrules": "^5.4.4",
     "@gamestdio/websocket": "^0.3.2",
     "@github/webauthn-json": "^2.1.1",
     "@hello-pangea/dnd": "^17.0.0",
     "@rails/ujs": "7.1.501",
+    "@react-spring/web": "^9.7.5",
     "@reduxjs/toolkit": "^2.0.1",
     "@svgr/webpack": "^5.5.0",
+    "@use-gesture/react": "^10.3.1",
     "arrow-key-navigation": "^1.2.0",
     "async-mutex": "^0.5.0",
     "axios": "^1.4.0",
     "babel-loader": "^8.3.0",
-    "babel-plugin-formatjs": "^10.5.1",
+    "babel-plugin-formatjs": "^10.5.37",
     "babel-plugin-lodash": "patch:babel-plugin-lodash@npm%3A3.3.4#~/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch",
     "babel-plugin-preval": "^5.1.0",
     "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
@@ -86,7 +89,7 @@
     "http-link-header": "^1.1.1",
     "immutable": "^4.3.0",
     "imports-loader": "^1.2.0",
-    "intl-messageformat": "^10.3.5",
+    "intl-messageformat": "^10.7.16",
     "js-yaml": "^4.1.0",
     "lande": "^1.0.10",
     "lodash": "^4.17.21",
@@ -105,9 +108,7 @@
     "react-hotkeys": "^1.1.4",
     "react-immutable-proptypes": "^2.2.0",
     "react-immutable-pure-component": "^2.2.2",
-    "react-intl": "^7.0.0",
-    "react-motion": "^0.5.2",
-    "react-notification": "^6.8.5",
+    "react-intl": "^7.1.10",
     "react-overlays": "^5.2.1",
     "react-redux": "^9.0.4",
     "react-redux-loading-bar": "^5.0.8",
@@ -146,13 +147,15 @@
     "workbox-window": "^7.0.0"
   },
   "devDependencies": {
+    "@eslint/js": "^9.23.0",
     "@formatjs/cli": "^6.1.1",
     "@testing-library/dom": "^10.2.0",
     "@testing-library/jest-dom": "^6.0.0",
     "@testing-library/react": "^16.0.0",
     "@types/babel__core": "^7.20.1",
-    "@types/emoji-mart": "^3.0.9",
+    "@types/emoji-mart": "3.0.14",
     "@types/escape-html": "^1.0.2",
+    "@types/eslint-plugin-jsx-a11y": "^6",
     "@types/hoist-non-react-statics": "^3.3.1",
     "@types/http-link-header": "^1.0.3",
     "@types/intl": "^1.2.0",
@@ -167,7 +170,6 @@
     "@types/react-dom": "^18.2.4",
     "@types/react-helmet": "^6.1.6",
     "@types/react-immutable-proptypes": "^2.1.0",
-    "@types/react-motion": "^0.0.40",
     "@types/react-router": "^5.1.20",
     "@types/react-router-dom": "^5.3.3",
     "@types/react-sparklines": "^1.7.2",
@@ -178,19 +180,17 @@
     "@types/requestidlecallback": "^0.3.5",
     "@types/webpack": "^4.41.33",
     "@types/webpack-env": "^1.18.4",
-    "@typescript-eslint/eslint-plugin": "^8.0.0",
-    "@typescript-eslint/parser": "^8.0.0",
     "babel-jest": "^29.5.0",
-    "eslint": "^8.41.0",
-    "eslint-define-config": "^2.0.0",
-    "eslint-import-resolver-typescript": "^3.5.5",
-    "eslint-plugin-formatjs": "^5.0.0",
-    "eslint-plugin-import": "~2.30.0",
-    "eslint-plugin-jsdoc": "^50.0.0",
-    "eslint-plugin-jsx-a11y": "~6.10.0",
-    "eslint-plugin-promise": "~7.1.0",
-    "eslint-plugin-react": "^7.33.2",
-    "eslint-plugin-react-hooks": "^5.0.0",
+    "eslint": "^9.23.0",
+    "eslint-import-resolver-typescript": "^4.2.5",
+    "eslint-plugin-formatjs": "^5.3.1",
+    "eslint-plugin-import": "~2.31.0",
+    "eslint-plugin-jsdoc": "^50.6.9",
+    "eslint-plugin-jsx-a11y": "~6.10.2",
+    "eslint-plugin-promise": "~7.2.1",
+    "eslint-plugin-react": "^7.37.4",
+    "eslint-plugin-react-hooks": "^5.2.0",
+    "globals": "^16.0.0",
     "husky": "^9.0.11",
     "jest": "^29.5.0",
     "jest-environment-jsdom": "^29.5.0",
@@ -200,7 +200,8 @@
     "stylelint": "^16.11.0",
     "stylelint-config-prettier-scss": "^1.0.0",
     "stylelint-config-standard-scss": "^14.0.0",
-    "typescript": "^5.0.4",
+    "typescript": "~5.7.3",
+    "typescript-eslint": "^8.28.0",
     "webpack-dev-server": "^3.11.3"
   },
   "resolutions": {
diff --git a/public/avatars/original/missing.png b/public/avatars/original/missing.png
index 781370782e..3b37e69c5d 100644
Binary files a/public/avatars/original/missing.png and b/public/avatars/original/missing.png differ
diff --git a/public/emoji/sheet_13.png b/public/emoji/sheet_13.png
deleted file mode 100644
index ade4077719..0000000000
Binary files a/public/emoji/sheet_13.png and /dev/null differ
diff --git a/public/emoji/sheet_15.png b/public/emoji/sheet_15.png
new file mode 100644
index 0000000000..3d0a679119
Binary files /dev/null and b/public/emoji/sheet_15.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
old mode 100755
new mode 100644
index fc5e475d42..7f865cfe96
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb
deleted file mode 100644
index feca543cb7..0000000000
--- a/spec/controllers/activitypub/inboxes_controller_spec.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe ActivityPub::InboxesController do
-  let(:remote_account) { nil }
-
-  before do
-    allow(controller).to receive(:signed_request_actor).and_return(remote_account)
-  end
-
-  describe 'POST #create' do
-    context 'with signature' do
-      let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) }
-
-      before do
-        post :create, body: '{}'
-      end
-
-      it 'returns http accepted' do
-        expect(response).to have_http_status(202)
-      end
-
-      context 'with a specific account' do
-        subject(:response) { post :create, params: { account_username: account.username }, body: '{}' }
-
-        let(:account) { Fabricate(:account) }
-
-        context 'when account is permanently suspended' do
-          before do
-            account.suspend!
-            account.deletion_request.destroy
-          end
-
-          it 'returns http gone' do
-            expect(response).to have_http_status(410)
-          end
-        end
-
-        context 'when account is temporarily suspended' do
-          before do
-            account.suspend!
-          end
-
-          it 'returns http accepted' do
-            expect(response).to have_http_status(202)
-          end
-        end
-      end
-    end
-
-    context 'with Collection-Synchronization header' do
-      let(:remote_account)             { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) }
-      let(:synchronization_collection) { remote_account.followers_url }
-      let(:synchronization_url)        { 'https://example.com/followers-for-domain' }
-      let(:synchronization_hash)       { 'somehash' }
-      let(:synchronization_header)     { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" }
-
-      before do
-        allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil)
-        allow(remote_account).to receive(:local_followers_hash).and_return('somehash')
-
-        request.headers['Collection-Synchronization'] = synchronization_header
-        post :create, body: '{}'
-      end
-
-      context 'with mismatching target collection' do
-        let(:synchronization_collection) { 'https://example.com/followers2' }
-
-        it 'does not start a synchronization job' do
-          expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async)
-        end
-      end
-
-      context 'with mismatching domain in partial collection attribute' do
-        let(:synchronization_url) { 'https://example.org/followers' }
-
-        it 'does not start a synchronization job' do
-          expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async)
-        end
-      end
-
-      context 'with matching digest' do
-        it 'does not start a synchronization job' do
-          expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async)
-        end
-      end
-
-      context 'with mismatching digest' do
-        let(:synchronization_hash) { 'wronghash' }
-
-        it 'starts a synchronization job' do
-          expect(ActivityPub::FollowersSynchronizationWorker).to have_received(:perform_async)
-        end
-      end
-
-      it 'returns http accepted' do
-        expect(response).to have_http_status(202)
-      end
-    end
-
-    context 'without signature' do
-      before do
-        post :create, body: '{}'
-      end
-
-      it 'returns http not authorized' do
-        expect(response).to have_http_status(401)
-      end
-    end
-  end
-end
diff --git a/spec/controllers/admin/account_actions_controller_spec.rb b/spec/controllers/admin/account_actions_controller_spec.rb
deleted file mode 100644
index fabe5cef4d..0000000000
--- a/spec/controllers/admin/account_actions_controller_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::AccountActionsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #new' do
-    let(:account) { Fabricate(:account) }
-
-    it 'returns http success' do
-      get :new, params: { account_id: account.id }
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-
-  describe 'POST #create' do
-    let(:account) { Fabricate(:account) }
-
-    it 'records the account action' do
-      expect do
-        post :create, params: { account_id: account.id, admin_account_action: { type: 'silence' } }
-      end.to change { account.strikes.count }.by(1)
-
-      expect(response).to redirect_to(admin_account_path(account.id))
-    end
-  end
-end
diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 72ca2275cf..2ec3bca746 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -59,16 +59,15 @@ RSpec.describe Admin::AccountsController do
       let(:account) { Fabricate(:account) }
 
       it 'includes moderation notes' do
-        note1 = Fabricate(:account_moderation_note, target_account: account)
-        note2 = Fabricate(:account_moderation_note, target_account: account)
+        note1 = Fabricate(:account_moderation_note, target_account: account, content: 'Note 1 remarks')
+        note2 = Fabricate(:account_moderation_note, target_account: account, content: 'Note 2 remarks')
 
         get :show, params: { id: account.id }
         expect(response).to have_http_status(200)
 
-        moderation_notes = assigns(:moderation_notes).to_a
-
-        expect(moderation_notes.size).to be 2
-        expect(moderation_notes).to eq [note1, note2]
+        expect(response.body)
+          .to include(note1.content)
+          .and include(note2.content)
       end
     end
 
diff --git a/spec/controllers/admin/base_controller_spec.rb b/spec/controllers/admin/base_controller_spec.rb
index 6e78ccb3b1..d739b54644 100644
--- a/spec/controllers/admin/base_controller_spec.rb
+++ b/spec/controllers/admin/base_controller_spec.rb
@@ -3,40 +3,54 @@
 require 'rails_helper'
 
 RSpec.describe Admin::BaseController do
+  render_views
+
   controller do
     def success
       authorize :dashboard, :index?
-      render 'admin/reports/show'
+      render html: '<p>success</p>', layout: true
     end
   end
 
-  it 'requires administrator or moderator' do
-    routes.draw { get 'success' => 'admin/base#success' }
-    sign_in(Fabricate(:user))
-    get :success
+  before { routes.draw { get 'success' => 'admin/base#success' } }
 
-    expect(response).to have_http_status(403)
+  context 'when signed in as regular user' do
+    before { sign_in Fabricate(:user) }
+
+    it 'responds with unauthorized' do
+      get :success
+
+      expect(response).to have_http_status(403)
+    end
   end
 
-  it 'returns private cache control headers' do
-    routes.draw { get 'success' => 'admin/base#success' }
-    sign_in(Fabricate(:moderator_user))
-    get :success
+  context 'when signed in as moderator' do
+    before { sign_in Fabricate(:moderator_user) }
 
-    expect(response.headers['Cache-Control']).to include('private, no-store')
+    it 'returns success with private headers and admin layout' do
+      get :success
+
+      expect(response)
+        .to have_http_status(200)
+      expect(response.headers['Cache-Control'])
+        .to include('private, no-store')
+      expect(response.parsed_body)
+        .to have_css('body.admin')
+    end
   end
 
-  it 'renders admin layout as a moderator' do
-    routes.draw { get 'success' => 'admin/base#success' }
-    sign_in(Fabricate(:moderator_user))
-    get :success
-    expect(response).to render_template layout: 'admin'
-  end
+  context 'when signed in as admin' do
+    before { sign_in Fabricate(:admin_user) }
 
-  it 'renders admin layout as an admin' do
-    routes.draw { get 'success' => 'admin/base#success' }
-    sign_in(Fabricate(:admin_user))
-    get :success
-    expect(response).to render_template layout: 'admin'
+    it 'returns success with private headers and admin layout' do
+      get :success
+
+      expect(response)
+        .to have_http_status(200)
+      expect(response.headers['Cache-Control'])
+        .to include('private, no-store')
+      expect(response.parsed_body)
+        .to have_css('body.admin')
+    end
   end
 end
diff --git a/spec/controllers/admin/change_emails_controller_spec.rb b/spec/controllers/admin/change_emails_controller_spec.rb
deleted file mode 100644
index 899106e54e..0000000000
--- a/spec/controllers/admin/change_emails_controller_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::ChangeEmailsController do
-  render_views
-
-  let(:admin) { Fabricate(:admin_user) }
-
-  before do
-    sign_in admin
-  end
-
-  describe 'GET #show' do
-    it 'returns http success' do
-      user = Fabricate(:user)
-
-      get :show, params: { account_id: user.account.id }
-
-      expect(response).to have_http_status(200)
-    end
-  end
-
-  describe 'GET #update' do
-    before do
-      allow(UserMailer).to receive(:confirmation_instructions)
-        .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil))
-    end
-
-    it 'returns http success' do
-      user = Fabricate(:user)
-
-      previous_email = user.email
-
-      post :update, params: { account_id: user.account.id, user: { unconfirmed_email: 'test@example.com' } }
-
-      user.reload
-
-      expect(user.email).to eq previous_email
-      expect(user.unconfirmed_email).to eq 'test@example.com'
-      expect(user.confirmation_token).to_not be_nil
-
-      expect(UserMailer).to have_received(:confirmation_instructions).with(user, user.confirmation_token, { to: 'test@example.com' })
-
-      expect(response).to redirect_to(admin_account_path(user.account.id))
-    end
-  end
-end
diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb
index f3f08c7940..bf3737bdb3 100644
--- a/spec/controllers/admin/domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/domain_blocks_controller_spec.rb
@@ -69,7 +69,8 @@ RSpec.describe Admin::DomainBlocksController do
 
         expect(DomainBlockWorker).to_not have_received(:perform_async)
 
-        expect(response).to render_template :new
+        expect(response.parsed_body.title)
+          .to match(I18n.t('admin.domain_blocks.new.title'))
       end
     end
 
@@ -84,7 +85,8 @@ RSpec.describe Admin::DomainBlocksController do
 
           expect(DomainBlockWorker).to_not have_received(:perform_async)
 
-          expect(response).to render_template :confirm_suspension
+          expect(response.parsed_body.title)
+            .to match(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
         end
       end
 
@@ -119,7 +121,8 @@ RSpec.describe Admin::DomainBlocksController do
 
           expect(DomainBlockWorker).to_not have_received(:perform_async)
 
-          expect(response).to render_template :confirm_suspension
+          expect(response.parsed_body.title)
+            .to match(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
         end
       end
 
diff --git a/spec/controllers/admin/export_domain_blocks_controller_spec.rb b/spec/controllers/admin/export_domain_blocks_controller_spec.rb
index b8e0696e55..1e748e34b6 100644
--- a/spec/controllers/admin/export_domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/export_domain_blocks_controller_spec.rb
@@ -44,11 +44,11 @@ RSpec.describe Admin::ExportDomainBlocksController do
       end
 
       it 'renders page with extended domain blocks' do
-        expect(assigns(:domain_blocks).map { |block| [block.domain, block.reject_favourite, block.reject_friend] }).to contain_exactly(
-          ['bad.domain', false, false],
-          ['worse.domain', false, false],
-          ['reject.media', false, false],
-          ['little.spam', true, false]
+        expect(mapped_batch_table_rows_with_expanded_params).to contain_exactly(
+          ['bad.domain', false],
+          ['worse.domain', false],
+          ['reject.media', false],
+          ['little.spam', true]
         )
       end
 
@@ -73,6 +73,10 @@ RSpec.describe Admin::ExportDomainBlocksController do
       batch_table_rows.map { |row| [row.at_css('[id$=_domain]')['value'], row.at_css('[id$=_severity]')['value'].to_sym] }
     end
 
+    def mapped_batch_table_rows_with_expanded_params
+      batch_table_rows.map { |row| [row.at_css('[id$=_domain]')['value'], row.at_css('[id$=_reject_favourite]')['value'] == 'true'] }
+    end
+
     def batch_table_rows
       response.parsed_body.css('body div.batch-table__row')
     end
diff --git a/spec/controllers/admin/instances_controller_spec.rb b/spec/controllers/admin/instances_controller_spec.rb
index 83655bafa0..b6508eb38b 100644
--- a/spec/controllers/admin/instances_controller_spec.rb
+++ b/spec/controllers/admin/instances_controller_spec.rb
@@ -49,23 +49,11 @@ RSpec.describe Admin::InstancesController do
 
       expect(response).to have_http_status(200)
 
-      instance = assigns(:instance)
-      expect(instance).to_not be_new_record
+      expect(response.body)
+        .to include(I18n.t('admin.instances.totals_time_period_hint_html'))
+        .and include(I18n.t('accounts.nothing_here'))
 
       expect(Admin::ActionLogFilter).to have_received(:new).with(target_domain: account_popular_main.domain)
-
-      action_logs = assigns(:action_logs).to_a
-      expect(action_logs.size).to eq 0
-    end
-
-    context 'with an unknown domain' do
-      it 'returns http success' do
-        get :show, params: { id: 'unknown.example' }
-        expect(response).to have_http_status(200)
-
-        instance = assigns(:instance)
-        expect(instance).to be_new_record
-      end
     end
   end
 
diff --git a/spec/controllers/admin/relationships_controller_spec.rb b/spec/controllers/admin/relationships_controller_spec.rb
deleted file mode 100644
index 1d300459f6..0000000000
--- a/spec/controllers/admin/relationships_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::RelationshipsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #index' do
-    let(:account) { Fabricate(:account) }
-
-    it 'returns http success' do
-      get :index, params: { account_id: account.id }
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-end
diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb
deleted file mode 100644
index 4012204de1..0000000000
--- a/spec/controllers/admin/reports_controller_spec.rb
+++ /dev/null
@@ -1,125 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::ReportsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #index' do
-    it 'returns http success with no filters' do
-      specified = Fabricate(:report, action_taken_at: nil, comment: 'First report')
-      other = Fabricate(:report, action_taken_at: Time.now.utc, comment: 'Second report')
-
-      get :index
-
-      expect(response).to have_http_status(200)
-      expect(response.body)
-        .to include(specified.comment)
-        .and not_include(other.comment)
-    end
-
-    it 'returns http success with resolved filter' do
-      specified = Fabricate(:report, action_taken_at: Time.now.utc, comment: 'First report')
-      other = Fabricate(:report, action_taken_at: nil, comment: 'Second report')
-
-      get :index, params: { resolved: '1' }
-
-      expect(response).to have_http_status(200)
-      expect(response.body)
-        .to include(specified.comment)
-        .and not_include(other.comment)
-    end
-  end
-
-  describe 'GET #show' do
-    it 'renders report' do
-      report = Fabricate(:report, comment: 'A big problem')
-
-      get :show, params: { id: report }
-
-      expect(response).to have_http_status(200)
-      expect(response.body)
-        .to include(report.comment)
-    end
-
-    describe 'account moderation notes' do
-      let(:report) { Fabricate(:report) }
-
-      it 'includes moderation notes' do
-        note1 = Fabricate(:report_note, report: report)
-        note2 = Fabricate(:report_note, report: report)
-
-        get :show, params: { id: report }
-
-        expect(response).to have_http_status(200)
-
-        report_notes = assigns(:report_notes).to_a
-
-        expect(report_notes.size).to be 2
-        expect(report_notes).to eq [note1, note2]
-      end
-    end
-  end
-
-  describe 'POST #resolve' do
-    it 'resolves the report' do
-      report = Fabricate(:report)
-
-      put :resolve, params: { id: report }
-      expect(response).to redirect_to(admin_reports_path)
-      report.reload
-      expect(report.action_taken_by_account).to eq user.account
-      expect(report.action_taken?).to be true
-      expect(last_action_log.target).to eq(report)
-    end
-  end
-
-  describe 'POST #reopen' do
-    it 'reopens the report' do
-      report = Fabricate(:report, action_taken_at: 3.days.ago)
-
-      put :reopen, params: { id: report }
-      expect(response).to redirect_to(admin_report_path(report))
-      report.reload
-      expect(report.action_taken_by_account).to be_nil
-      expect(report.action_taken?).to be false
-      expect(last_action_log.target).to eq(report)
-    end
-  end
-
-  describe 'POST #assign_to_self' do
-    it 'reopens the report' do
-      report = Fabricate(:report)
-
-      put :assign_to_self, params: { id: report }
-      expect(response).to redirect_to(admin_report_path(report))
-      report.reload
-      expect(report.assigned_account).to eq user.account
-      expect(last_action_log.target).to eq(report)
-    end
-  end
-
-  describe 'POST #unassign' do
-    it 'reopens the report' do
-      report = Fabricate(:report, assigned_account_id: Account.last.id)
-
-      put :unassign, params: { id: report }
-      expect(response).to redirect_to(admin_report_path(report))
-      report.reload
-      expect(report.assigned_account).to be_nil
-      expect(last_action_log.target).to eq(report)
-    end
-  end
-
-  private
-
-  def last_action_log
-    Admin::ActionLog.last
-  end
-end
diff --git a/spec/controllers/admin/roles_controller_spec.rb b/spec/controllers/admin/roles_controller_spec.rb
deleted file mode 100644
index 173a89e5d5..0000000000
--- a/spec/controllers/admin/roles_controller_spec.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::RolesController do
-  render_views
-
-  let(:permissions)  { UserRole::Flags::NONE }
-  let(:current_role) { UserRole.create(name: 'Foo', permissions: permissions, position: 10) }
-  let(:current_user) { Fabricate(:user, role: current_role) }
-
-  before do
-    sign_in current_user, scope: :user
-  end
-
-  describe 'GET #index' do
-    before do
-      get :index
-    end
-
-    context 'when user does not have permission to manage roles' do
-      it 'returns http forbidden' do
-        expect(response).to have_http_status(403)
-      end
-    end
-
-    context 'when user has permission to manage roles' do
-      let(:permissions) { UserRole::FLAGS[:manage_roles] }
-
-      it 'returns http success' do
-        expect(response).to have_http_status(:success)
-      end
-    end
-  end
-
-  describe 'GET #new' do
-    before do
-      get :new
-    end
-
-    context 'when user does not have permission to manage roles' do
-      it 'returns http forbidden' do
-        expect(response).to have_http_status(403)
-      end
-    end
-
-    context 'when user has permission to manage roles' do
-      let(:permissions) { UserRole::FLAGS[:manage_roles] }
-
-      it 'returns http success' do
-        expect(response).to have_http_status(:success)
-      end
-    end
-  end
-
-  describe 'POST #create' do
-    let(:selected_position) { 1 }
-    let(:selected_permissions_as_keys) { %w(manage_roles) }
-
-    before do
-      post :create, params: { user_role: { name: 'Bar', position: selected_position, permissions_as_keys: selected_permissions_as_keys } }
-    end
-
-    context 'when user has permission to manage roles' do
-      let(:permissions) { UserRole::FLAGS[:manage_roles] }
-
-      context 'when new role\'s does not elevate above the user\'s role' do
-        let(:selected_position) { 1 }
-        let(:selected_permissions_as_keys) { %w(manage_roles) }
-
-        it 'redirects to roles page and creates role' do
-          expect(response).to redirect_to(admin_roles_path)
-
-          expect(UserRole.find_by(name: 'Bar')).to_not be_nil
-        end
-      end
-
-      context 'when new role\'s position is higher than user\'s role' do
-        let(:selected_position) { 100 }
-        let(:selected_permissions_as_keys) { %w(manage_roles) }
-
-        it 'renders new template and does not create role' do
-          expect(response).to render_template(:new)
-
-          expect(UserRole.find_by(name: 'Bar')).to be_nil
-        end
-      end
-
-      context 'when new role has permissions the user does not have' do
-        let(:selected_position) { 1 }
-        let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
-
-        it 'renders new template and does not create role' do
-          expect(response).to render_template(:new)
-
-          expect(UserRole.find_by(name: 'Bar')).to be_nil
-        end
-      end
-
-      context 'when user has administrator permission' do
-        let(:permissions) { UserRole::FLAGS[:administrator] }
-
-        let(:selected_position) { 1 }
-        let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
-
-        it 'redirects to roles page and creates new role' do
-          expect(response).to redirect_to(admin_roles_path)
-
-          expect(UserRole.find_by(name: 'Bar')).to_not be_nil
-        end
-      end
-    end
-  end
-
-  describe 'GET #edit' do
-    let(:role_position) { 8 }
-    let(:role) { UserRole.create(name: 'Bar', permissions: UserRole::FLAGS[:manage_users], position: role_position) }
-
-    before do
-      get :edit, params: { id: role.id }
-    end
-
-    context 'when user does not have permission to manage roles' do
-      it 'returns http forbidden' do
-        expect(response).to have_http_status(403)
-      end
-    end
-
-    context 'when user has permission to manage roles' do
-      let(:permissions) { UserRole::FLAGS[:manage_roles] }
-
-      context 'when user outranks the role' do
-        it 'returns http success' do
-          expect(response).to have_http_status(:success)
-        end
-      end
-
-      context 'when role outranks user' do
-        let(:role_position) { current_role.position + 1 }
-
-        it 'returns http forbidden' do
-          expect(response).to have_http_status(403)
-        end
-      end
-    end
-  end
-
-  describe 'PUT #update' do
-    let(:role_position) { 8 }
-    let(:role_permissions) { UserRole::FLAGS[:manage_users] }
-    let(:role) { UserRole.create(name: 'Bar', permissions: role_permissions, position: role_position) }
-
-    let(:selected_position) { 8 }
-    let(:selected_permissions_as_keys) { %w(manage_users) }
-
-    before do
-      put :update, params: { id: role.id, user_role: { name: 'Baz', position: selected_position, permissions_as_keys: selected_permissions_as_keys } }
-    end
-
-    context 'when user does not have permission to manage roles' do
-      it 'returns http forbidden and does not update role' do
-        expect(response).to have_http_status(403)
-
-        expect(role.reload.name).to eq 'Bar'
-      end
-    end
-
-    context 'when user has permission to manage roles' do
-      let(:permissions) { UserRole::FLAGS[:manage_roles] }
-
-      context 'when role has permissions the user doesn\'t' do
-        it 'renders edit template and does not update role' do
-          expect(response).to render_template(:edit)
-
-          expect(role.reload.name).to eq 'Bar'
-        end
-      end
-
-      context 'when user has all permissions of the role' do
-        let(:permissions) { UserRole::FLAGS[:manage_roles] | UserRole::FLAGS[:manage_users] }
-
-        context 'when user outranks the role' do
-          it 'redirects to roles page and updates role' do
-            expect(response).to redirect_to(admin_roles_path)
-
-            expect(role.reload.name).to eq 'Baz'
-          end
-        end
-
-        context 'when role outranks user' do
-          let(:role_position) { current_role.position + 1 }
-
-          it 'returns http forbidden and does not update role' do
-            expect(response).to have_http_status(403)
-
-            expect(role.reload.name).to eq 'Bar'
-          end
-        end
-      end
-    end
-  end
-
-  describe 'DELETE #destroy' do
-    let(:role_position) { 8 }
-    let(:role) { UserRole.create(name: 'Bar', permissions: UserRole::FLAGS[:manage_users], position: role_position) }
-
-    before do
-      delete :destroy, params: { id: role.id }
-    end
-
-    context 'when user does not have permission to manage roles' do
-      it 'returns http forbidden' do
-        expect(response).to have_http_status(403)
-      end
-    end
-
-    context 'when user has permission to manage roles' do
-      let(:permissions) { UserRole::FLAGS[:manage_roles] }
-
-      context 'when user outranks the role' do
-        it 'redirects to roles page' do
-          expect(response).to redirect_to(admin_roles_path)
-        end
-      end
-
-      context 'when role outranks user' do
-        let(:role_position) { current_role.position + 1 }
-
-        it 'returns http forbidden' do
-          expect(response).to have_http_status(403)
-        end
-      end
-    end
-  end
-end
diff --git a/spec/controllers/admin/settings/branding_controller_spec.rb b/spec/controllers/admin/settings/branding_controller_spec.rb
deleted file mode 100644
index 6b3621bb8a..0000000000
--- a/spec/controllers/admin/settings/branding_controller_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::Settings::BrandingController do
-  render_views
-
-  describe 'When signed in as an admin' do
-    before do
-      sign_in Fabricate(:admin_user), scope: :user
-    end
-
-    describe 'PUT #update' do
-      it 'cannot create a setting value for a non-admin key' do
-        expect(Setting.new_setting_key).to be_blank
-
-        patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } }
-
-        expect(response)
-          .to have_http_status(400)
-        expect(Setting.new_setting_key).to be_nil
-      end
-    end
-  end
-end
diff --git a/spec/controllers/admin/site_uploads_controller_spec.rb b/spec/controllers/admin/site_uploads_controller_spec.rb
deleted file mode 100644
index 1fe5903498..0000000000
--- a/spec/controllers/admin/site_uploads_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::SiteUploadsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'DELETE #destroy' do
-    let(:site_upload) { Fabricate(:site_upload, var: 'thumbnail') }
-
-    it 'returns http success' do
-      delete :destroy, params: { id: site_upload.id }
-
-      expect(response).to redirect_to(admin_settings_path)
-    end
-  end
-end
diff --git a/spec/controllers/admin/terms_of_service/distributions_controller_spec.rb b/spec/controllers/admin/terms_of_service/distributions_controller_spec.rb
deleted file mode 100644
index 63431c1336..0000000000
--- a/spec/controllers/admin/terms_of_service/distributions_controller_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::TermsOfService::DistributionsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-  let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'POST #create' do
-    it 'returns http success' do
-      post :create, params: { terms_of_service_id: terms_of_service.id }
-
-      expect(response).to redirect_to(admin_terms_of_service_index_path)
-    end
-  end
-end
diff --git a/spec/controllers/admin/terms_of_service/drafts_controller_spec.rb b/spec/controllers/admin/terms_of_service/drafts_controller_spec.rb
deleted file mode 100644
index 0a9c6e6b67..0000000000
--- a/spec/controllers/admin/terms_of_service/drafts_controller_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::TermsOfService::DraftsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #show' do
-    it 'returns http success' do
-      get :show
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-
-  describe 'PUT #update' do
-    subject { put :update, params: params }
-
-    let!(:terms) { Fabricate :terms_of_service, published_at: nil }
-
-    context 'with publishing params' do
-      let(:params) { { terms_of_service: { text: 'new' }, action_type: 'publish' } }
-
-      it 'publishes the record' do
-        expect { subject }
-          .to change(Admin::ActionLog, :count).by(1)
-
-        expect(response)
-          .to redirect_to(admin_terms_of_service_index_path)
-        expect(terms.reload.published_at)
-          .to_not be_nil
-      end
-    end
-
-    context 'with non publishing params' do
-      let(:params) { { terms_of_service: { text: 'new' }, action_type: 'save_draft' } }
-
-      it 'updates but does not publish the record' do
-        expect { subject }
-          .to_not change(Admin::ActionLog, :count)
-
-        expect(response)
-          .to redirect_to(admin_terms_of_service_draft_path)
-        expect(terms.reload.published_at)
-          .to be_nil
-      end
-    end
-
-    context 'with invalid params' do
-      let(:params) { { terms_of_service: { text: '' }, action_type: 'save_draft' } }
-
-      it 'does not update the record' do
-        subject
-
-        expect(response)
-          .to have_http_status(:success)
-      end
-    end
-  end
-end
diff --git a/spec/controllers/admin/terms_of_service/generates_controller_spec.rb b/spec/controllers/admin/terms_of_service/generates_controller_spec.rb
deleted file mode 100644
index c1fc7faef9..0000000000
--- a/spec/controllers/admin/terms_of_service/generates_controller_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::TermsOfService::GeneratesController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #show' do
-    it 'returns http success' do
-      get :show
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-
-  describe 'POST #create' do
-    subject { post :create, params: params }
-
-    context 'with valid params' do
-      let(:params) do
-        {
-          terms_of_service_generator: {
-            admin_email: 'test@host.example',
-            arbitration_address: '123 Main Street',
-            arbitration_website: 'https://host.example',
-            dmca_address: '123 DMCA Ave',
-            dmca_email: 'dmca@host.example',
-            domain: 'host.example',
-            jurisdiction: 'Europe',
-          },
-        }
-      end
-
-      it 'saves new record' do
-        expect { subject }
-          .to change(TermsOfService, :count).by(1)
-        expect(response)
-          .to redirect_to(admin_terms_of_service_draft_path)
-      end
-    end
-
-    context 'with invalid params' do
-      let(:params) do
-        {
-          terms_of_service_generator: {
-            admin_email: 'what the',
-          },
-        }
-      end
-
-      it 'does not save new record' do
-        expect { subject }
-          .to_not change(TermsOfService, :count)
-        expect(response)
-          .to have_http_status(200)
-      end
-    end
-  end
-end
diff --git a/spec/controllers/admin/terms_of_service/previews_controller_spec.rb b/spec/controllers/admin/terms_of_service/previews_controller_spec.rb
deleted file mode 100644
index 3878bb4b6f..0000000000
--- a/spec/controllers/admin/terms_of_service/previews_controller_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::TermsOfService::PreviewsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-  let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #show' do
-    it 'returns http success' do
-      get :show, params: { terms_of_service_id: terms_of_service.id }
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-end
diff --git a/spec/controllers/admin/terms_of_service/tests_controller_spec.rb b/spec/controllers/admin/terms_of_service/tests_controller_spec.rb
deleted file mode 100644
index 777f699448..0000000000
--- a/spec/controllers/admin/terms_of_service/tests_controller_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::TermsOfService::TestsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-  let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'POST #create' do
-    it 'returns http success' do
-      post :create, params: { terms_of_service_id: terms_of_service.id }
-
-      expect(response).to redirect_to(admin_terms_of_service_preview_path(terms_of_service))
-    end
-  end
-end
diff --git a/spec/controllers/admin/terms_of_service_controller_spec.rb b/spec/controllers/admin/terms_of_service_controller_spec.rb
deleted file mode 100644
index feefd312e3..0000000000
--- a/spec/controllers/admin/terms_of_service_controller_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::TermsOfServiceController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-end
diff --git a/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb b/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb
deleted file mode 100644
index aadf002dd6..0000000000
--- a/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::Trends::Links::PreviewCardProvidersController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-end
diff --git a/spec/controllers/admin/trends/links_controller_spec.rb b/spec/controllers/admin/trends/links_controller_spec.rb
deleted file mode 100644
index cd2cf6b852..0000000000
--- a/spec/controllers/admin/trends/links_controller_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::Trends::LinksController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-end
diff --git a/spec/controllers/admin/trends/statuses_controller_spec.rb b/spec/controllers/admin/trends/statuses_controller_spec.rb
deleted file mode 100644
index 6570340b2d..0000000000
--- a/spec/controllers/admin/trends/statuses_controller_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::Trends::StatusesController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-end
diff --git a/spec/controllers/admin/trends/tags_controller_spec.rb b/spec/controllers/admin/trends/tags_controller_spec.rb
deleted file mode 100644
index 83ea23ed21..0000000000
--- a/spec/controllers/admin/trends/tags_controller_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::Trends::TagsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index
-
-      expect(response).to have_http_status(:success)
-    end
-  end
-end
diff --git a/spec/controllers/admin/users/roles_controller_spec.rb b/spec/controllers/admin/users/roles_controller_spec.rb
deleted file mode 100644
index a7d59181d6..0000000000
--- a/spec/controllers/admin/users/roles_controller_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::Users::RolesController do
-  render_views
-
-  let(:current_role) { UserRole.create(name: 'Foo', permissions: UserRole::FLAGS[:manage_roles], position: 10) }
-  let(:current_user) { Fabricate(:user, role: current_role) }
-
-  let(:previous_role) { nil }
-  let(:user) { Fabricate(:user, role: previous_role) }
-
-  before do
-    sign_in current_user, scope: :user
-  end
-
-  describe 'GET #show' do
-    before do
-      get :show, params: { user_id: user.id }
-    end
-
-    it 'returns http success' do
-      expect(response).to have_http_status(:success)
-    end
-
-    context 'when target user is higher ranked than current user' do
-      let(:previous_role) { UserRole.create(name: 'Baz', permissions: UserRole::FLAGS[:administrator], position: 100) }
-
-      it 'returns http forbidden' do
-        expect(response).to have_http_status(403)
-      end
-    end
-  end
-
-  describe 'PUT #update' do
-    let(:selected_role) { UserRole.create(name: 'Bar', permissions: permissions, position: position) }
-
-    before do
-      put :update, params: { user_id: user.id, user: { role_id: selected_role.id } }
-    end
-
-    context 'with manage roles permissions' do
-      let(:permissions) { UserRole::FLAGS[:manage_roles] }
-      let(:position) { 1 }
-
-      it 'updates user role and redirects' do
-        expect(user.reload.role_id).to eq selected_role&.id
-
-        expect(response).to redirect_to(admin_account_path(user.account_id))
-      end
-    end
-
-    context 'when selected role has higher position than current user\'s role' do
-      let(:permissions) { UserRole::FLAGS[:administrator] }
-      let(:position) { 100 }
-
-      it 'does not update user role and renders edit' do
-        expect(user.reload.role_id).to eq previous_role&.id
-
-        expect(response).to render_template(:show)
-      end
-    end
-
-    context 'when target user is higher ranked than current user' do
-      let(:previous_role) { UserRole.create(name: 'Baz', permissions: UserRole::FLAGS[:administrator], position: 100) }
-      let(:permissions) { UserRole::FLAGS[:manage_roles] }
-      let(:position) { 1 }
-
-      it 'does not update user role and returns http forbidden' do
-        expect(user.reload.role_id).to eq previous_role&.id
-
-        expect(response).to have_http_status(403)
-      end
-    end
-  end
-end
diff --git a/spec/controllers/admin/users/two_factor_authentications_controller_spec.rb b/spec/controllers/admin/users/two_factor_authentications_controller_spec.rb
deleted file mode 100644
index 39af2ca914..0000000000
--- a/spec/controllers/admin/users/two_factor_authentications_controller_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-require 'webauthn/fake_client'
-
-RSpec.describe Admin::Users::TwoFactorAuthenticationsController do
-  render_views
-
-  let(:user) { Fabricate(:user) }
-
-  before do
-    sign_in Fabricate(:admin_user), scope: :user
-  end
-
-  describe 'DELETE #destroy' do
-    context 'when user has OTP enabled' do
-      before do
-        user.update(otp_required_for_login: true)
-      end
-
-      it 'redirects to admin account page' do
-        delete :destroy, params: { user_id: user.id }
-
-        user.reload
-        expect(user.otp_enabled?).to be false
-        expect(response).to redirect_to(admin_account_path(user.account_id))
-      end
-    end
-
-    context 'when user has OTP and WebAuthn enabled' do
-      let(:fake_client) { WebAuthn::FakeClient.new('http://test.host') }
-
-      before do
-        user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id)
-
-        public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
-        Fabricate(:webauthn_credential,
-                  user_id: user.id,
-                  external_id: public_key_credential.id,
-                  public_key: public_key_credential.public_key,
-                  nickname: 'Security Key')
-      end
-
-      it 'redirects to admin account page' do
-        delete :destroy, params: { user_id: user.id }
-
-        user.reload
-        expect(user.otp_enabled?).to be false
-        expect(user.webauthn_enabled?).to be false
-        expect(response).to redirect_to(admin_account_path(user.account_id))
-      end
-    end
-  end
-end
diff --git a/spec/controllers/admin/webhooks/secrets_controller_spec.rb b/spec/controllers/admin/webhooks/secrets_controller_spec.rb
deleted file mode 100644
index da3d4f1438..0000000000
--- a/spec/controllers/admin/webhooks/secrets_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::Webhooks::SecretsController do
-  render_views
-
-  let(:user) { Fabricate(:admin_user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'POST #rotate' do
-    let(:webhook) { Fabricate(:webhook) }
-
-    it 'returns http success' do
-      post :rotate, params: { webhook_id: webhook.id }
-
-      expect(response).to redirect_to(admin_webhook_path(webhook))
-    end
-  end
-end
diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb
index acc0312113..1e01709262 100644
--- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb
+++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe Api::Web::PushSubscriptionsController do
           p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=',
           auth: 'eH_C8rq2raXqlcBVDa1gLg==',
         },
+        standard: standard,
       },
     }
   end
@@ -36,6 +37,7 @@ RSpec.describe Api::Web::PushSubscriptionsController do
       },
     }
   end
+  let(:standard) { '1' }
 
   before do
     sign_in(user)
@@ -51,14 +53,27 @@ RSpec.describe Api::Web::PushSubscriptionsController do
 
       user.reload
 
-      expect(created_push_subscription).to have_attributes(
-        endpoint: eq(create_payload[:subscription][:endpoint]),
-        key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]),
-        key_auth: eq(create_payload[:subscription][:keys][:auth])
-      )
+      expect(created_push_subscription)
+        .to have_attributes(
+          endpoint: eq(create_payload[:subscription][:endpoint]),
+          key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]),
+          key_auth: eq(create_payload[:subscription][:keys][:auth])
+        )
+        .and be_standard
       expect(user.session_activations.first.web_push_subscription).to eq(created_push_subscription)
     end
 
+    context 'when standard is provided as false value' do
+      let(:standard) { '0' }
+
+      it 'saves push subscription with standard as false' do
+        post :create, format: :json, params: create_payload
+
+        expect(created_push_subscription)
+          .to_not be_standard
+      end
+    end
+
     context 'with a user who has a session with a prior subscription' do
       let!(:prior_subscription) { Fabricate(:web_push_subscription, session_activation: user.session_activations.last) }
 
diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb
deleted file mode 100644
index 90095ac4b8..0000000000
--- a/spec/controllers/auth/passwords_controller_spec.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Auth::PasswordsController do
-  include Devise::Test::ControllerHelpers
-
-  describe 'GET #new' do
-    it 'returns http success' do
-      request.env['devise.mapping'] = Devise.mappings[:user]
-      get :new
-      expect(response).to have_http_status(200)
-    end
-  end
-
-  describe 'GET #edit' do
-    let(:user) { Fabricate(:user) }
-
-    before do
-      request.env['devise.mapping'] = Devise.mappings[:user]
-    end
-
-    context 'with valid reset_password_token' do
-      it 'returns http success' do
-        token = user.send_reset_password_instructions
-
-        get :edit, params: { reset_password_token: token }
-
-        expect(response).to have_http_status(200)
-      end
-    end
-
-    context 'with invalid reset_password_token' do
-      it 'redirects to #new' do
-        get :edit, params: { reset_password_token: 'some_invalid_value' }
-        expect(response).to redirect_to subject.new_password_path(subject.send(:resource_name))
-      end
-    end
-  end
-
-  describe 'POST #update' do
-    let(:user) { Fabricate(:user) }
-    let(:password) { 'reset0password' }
-
-    before do
-      request.env['devise.mapping'] = Devise.mappings[:user]
-    end
-
-    context 'with valid reset_password_token' do
-      let!(:session_activation) { Fabricate(:session_activation, user: user) }
-      let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
-      let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
-
-      before do
-        token = user.send_reset_password_instructions
-
-        post :update, params: { user: { password: password, password_confirmation: password, reset_password_token: token } }
-      end
-
-      it 'resets the password' do
-        expect(response)
-          .to redirect_to '/auth/sign_in'
-
-        # Change password
-        expect(User.find(user.id))
-          .to be_present
-          .and be_valid_password(password)
-
-        # Deactivate session
-        expect(user.session_activations.count)
-          .to eq 0
-        expect { session_activation.reload }
-          .to raise_error(ActiveRecord::RecordNotFound)
-
-        # Revoke tokens
-        expect(Doorkeeper::AccessToken.active_for(user).count)
-          .to eq 0
-
-        # Remove push subs
-        expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count)
-          .to eq 0
-        expect { web_push_subscription.reload }
-          .to raise_error(ActiveRecord::RecordNotFound)
-      end
-    end
-
-    context 'with invalid reset_password_token' do
-      before do
-        post :update, params: { user: { password: password, password_confirmation: password, reset_password_token: 'some_invalid_value' } }
-      end
-
-      it 'renders reset password and retains password' do
-        expect(response)
-          .to render_template(:new)
-
-        expect(User.find(user.id))
-          .to be_present
-          .and be_external_or_valid_password(user.password)
-      end
-    end
-  end
-end
diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb
index a16e933cf3..e7a8dd6d7f 100644
--- a/spec/controllers/auth/registrations_controller_spec.rb
+++ b/spec/controllers/auth/registrations_controller_spec.rb
@@ -342,6 +342,42 @@ RSpec.describe Auth::RegistrationsController do
       end
     end
 
+    context 'when age verification is enabled' do
+      subject { post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' }.merge(date_of_birth) } }
+
+      before do
+        Setting.min_age = 16
+      end
+
+      let(:date_of_birth) { {} }
+
+      context 'when date of birth is below age limit' do
+        let(:date_of_birth) { 13.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } }
+
+        it 'does not create user' do
+          subject
+          user = User.find_by(email: 'test@example.com')
+          expect(user).to be_nil
+        end
+      end
+
+      context 'when date of birth is above age limit' do
+        let(:date_of_birth) { 17.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } }
+
+        it 'redirects to setup and creates user' do
+          subject
+
+          expect(response).to redirect_to auth_setup_path
+
+          expect(User.find_by(email: 'test@example.com'))
+            .to be_present
+            .and have_attributes(
+              age_verified_at: not_eq(nil)
+            )
+        end
+      end
+    end
+
     context 'when max user count is set' do
       subject do
         post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' } }
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index d4769a4722..9ecae92801 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -262,8 +262,8 @@ RSpec.describe Auth::SessionsController do
           end
 
           it 'renders two factor authentication page' do
-            expect(controller).to render_template('two_factor')
-            expect(controller).to render_template(partial: '_otp_authentication_form')
+            expect(response.body)
+              .to include(I18n.t('simple_form.hints.sessions.otp'))
           end
         end
 
@@ -278,8 +278,8 @@ RSpec.describe Auth::SessionsController do
           end
 
           it 'renders two factor authentication page' do
-            expect(controller).to render_template('two_factor')
-            expect(controller).to render_template(partial: '_otp_authentication_form')
+            expect(response.body)
+              .to include(I18n.t('simple_form.hints.sessions.otp'))
           end
         end
 
@@ -289,8 +289,8 @@ RSpec.describe Auth::SessionsController do
           end
 
           it 'renders two factor authentication page' do
-            expect(controller).to render_template('two_factor')
-            expect(controller).to render_template(partial: '_otp_authentication_form')
+            expect(response.body)
+              .to include(I18n.t('simple_form.hints.sessions.otp'))
           end
         end
 
@@ -417,8 +417,8 @@ RSpec.describe Auth::SessionsController do
           end
 
           it 'renders webauthn authentication page' do
-            expect(controller).to render_template('two_factor')
-            expect(controller).to render_template(partial: '_webauthn_form')
+            expect(response.body)
+              .to include(I18n.t('simple_form.title.sessions.webauthn'))
           end
         end
 
@@ -428,8 +428,8 @@ RSpec.describe Auth::SessionsController do
           end
 
           it 'renders webauthn authentication page' do
-            expect(controller).to render_template('two_factor')
-            expect(controller).to render_template(partial: '_webauthn_form')
+            expect(response.body)
+              .to include(I18n.t('simple_form.title.sessions.webauthn'))
           end
         end
 
diff --git a/spec/controllers/concerns/api/rate_limit_headers_spec.rb b/spec/controllers/concerns/api/rate_limit_headers_spec.rb
index 74ed81f5da..dd16531350 100644
--- a/spec/controllers/concerns/api/rate_limit_headers_spec.rb
+++ b/spec/controllers/concerns/api/rate_limit_headers_spec.rb
@@ -40,15 +40,11 @@ RSpec.describe Api::RateLimitHeaders do
         end
       end
 
-      it 'applies rate limiting limit header' do
+      it 'provides rate limit information in headers' do
         expect(response.headers['X-RateLimit-Limit']).to eq '100'
-      end
 
-      it 'applies rate limiting remaining header' do
         expect(response.headers['X-RateLimit-Remaining']).to eq '80'
-      end
 
-      it 'applies rate limiting reset header' do
         expect(response.headers['X-RateLimit-Reset']).to eq (start_time + 10.seconds).iso8601(6)
       end
     end
diff --git a/spec/controllers/concerns/challengable_concern_spec.rb b/spec/controllers/concerns/challengable_concern_spec.rb
index 01f979ff4c..823003c8de 100644
--- a/spec/controllers/concerns/challengable_concern_spec.rb
+++ b/spec/controllers/concerns/challengable_concern_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe ChallengableConcern do
+  render_views
+
   controller(ApplicationController) do
     include ChallengableConcern # rubocop:disable RSpec/DescribedClass
 
@@ -85,29 +87,35 @@ RSpec.describe ChallengableConcern do
       before { get :foo }
 
       it 'renders challenge' do
-        expect(response).to render_template('auth/challenges/new', layout: :auth)
+        expect(response.parsed_body)
+          .to have_title(I18n.t('challenge.prompt'))
       end
-
-      # See Auth::ChallengesControllerSpec
     end
 
     context 'with POST requests' do
       before { post :bar }
 
       it 'renders challenge' do
-        expect(response).to render_template('auth/challenges/new', layout: :auth)
+        expect(response.parsed_body)
+          .to have_title(I18n.t('challenge.prompt'))
       end
 
       it 'accepts correct password' do
         post :bar, params: { form_challenge: { current_password: password } }
-        expect(response.body).to eq 'bar'
-        expect(session[:challenge_passed_at]).to_not be_nil
+
+        expect(response.body)
+          .to eq 'bar'
+        expect(session[:challenge_passed_at])
+          .to_not be_nil
       end
 
       it 'rejects wrong password' do
         post :bar, params: { form_challenge: { current_password: 'dddfff888123' } }
-        expect(response.body).to render_template('auth/challenges/new', layout: :auth)
-        expect(session[:challenge_passed_at]).to be_nil
+
+        expect(response.parsed_body)
+          .to have_title(I18n.t('challenge.prompt'))
+        expect(session[:challenge_passed_at])
+          .to be_nil
       end
     end
   end
diff --git a/spec/controllers/disputes/strikes_controller_spec.rb b/spec/controllers/disputes/strikes_controller_spec.rb
deleted file mode 100644
index f6d28fc09a..0000000000
--- a/spec/controllers/disputes/strikes_controller_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Disputes::StrikesController do
-  render_views
-
-  before { sign_in current_user, scope: :user }
-
-  describe '#show' do
-    let(:current_user) { Fabricate(:user) }
-    let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
-
-    before do
-      get :show, params: { id: strike.id }
-    end
-
-    context 'when meant for the user' do
-      it 'returns http success' do
-        expect(response).to have_http_status(:success)
-      end
-    end
-
-    context 'when meant for a different user' do
-      let(:strike) { Fabricate(:account_warning) }
-
-      it 'returns http forbidden' do
-        expect(response).to have_http_status(403)
-      end
-    end
-  end
-end
diff --git a/spec/controllers/filters/statuses_controller_spec.rb b/spec/controllers/filters/statuses_controller_spec.rb
deleted file mode 100644
index f1fed76fca..0000000000
--- a/spec/controllers/filters/statuses_controller_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Filters::StatusesController do
-  render_views
-
-  describe 'GET #index' do
-    let(:filter) { Fabricate(:custom_filter) }
-
-    context 'with signed out user' do
-      it 'redirects' do
-        get :index, params: { filter_id: filter }
-
-        expect(response).to be_redirect
-      end
-    end
-
-    context 'with a signed in user' do
-      context 'with the filter user signed in' do
-        before do
-          sign_in(filter.account.user)
-          get :index, params: { filter_id: filter }
-        end
-
-        it 'returns http success' do
-          expect(response).to have_http_status(200)
-        end
-
-        it 'returns private cache control headers' do
-          expect(response.headers['Cache-Control']).to include('private, no-store')
-        end
-      end
-
-      context 'with another user signed in' do
-        before do
-          sign_in(Fabricate(:user))
-          get :index, params: { filter_id: filter }
-        end
-
-        it 'returns http not found' do
-          expect(response).to have_http_status(404)
-        end
-      end
-    end
-  end
-end
diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb
deleted file mode 100644
index a2eed797e0..0000000000
--- a/spec/controllers/oauth/tokens_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Oauth::TokensController do
-  describe 'POST #revoke' do
-    let!(:user) { Fabricate(:user) }
-    let!(:application) { Fabricate(:application, confidential: false) }
-    let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) }
-    let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
-
-    it 'revokes the token and removes subscriptions' do
-      post :revoke, params: { client_id: application.uid, token: access_token.token }
-
-      expect(access_token.reload.revoked_at)
-        .to_not be_nil
-      expect(Web::PushSubscription.where(access_token: access_token).count)
-        .to eq(0)
-      expect { web_push_subscription.reload }
-        .to raise_error(ActiveRecord::RecordNotFound)
-    end
-  end
-end
diff --git a/spec/controllers/relationships_controller_spec.rb b/spec/controllers/relationships_controller_spec.rb
index 323fcc995d..75b5e71f35 100644
--- a/spec/controllers/relationships_controller_spec.rb
+++ b/spec/controllers/relationships_controller_spec.rb
@@ -14,11 +14,9 @@ RSpec.describe RelationshipsController do
         get :show, params: { page: 2, relationship: 'followed_by' }
       end
 
-      it 'returns http success' do
+      it 'returns http success and private cache control headers' do
         expect(response).to have_http_status(200)
-      end
 
-      it 'returns private cache control headers' do
         expect(response.headers['Cache-Control']).to include('private, no-store')
       end
     end
diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb
deleted file mode 100644
index 98104b8454..0000000000
--- a/spec/controllers/settings/deletes_controller_spec.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Settings::DeletesController do
-  render_views
-
-  describe 'GET #show' do
-    context 'when signed in' do
-      let(:user) { Fabricate(:user) }
-
-      before do
-        sign_in user, scope: :user
-        get :show
-      end
-
-      it 'renders confirmation page with private cache control headers', :aggregate_failures do
-        expect(response).to have_http_status(200)
-        expect(response.headers['Cache-Control']).to include('private, no-store')
-      end
-
-      context 'when suspended' do
-        let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
-
-        it 'returns http forbidden with private cache control headers', :aggregate_failures do
-          expect(response).to have_http_status(403)
-          expect(response.headers['Cache-Control']).to include('private, no-store')
-        end
-      end
-    end
-
-    context 'when not signed in' do
-      it 'redirects' do
-        get :show
-        expect(response).to redirect_to '/auth/sign_in'
-      end
-    end
-  end
-
-  describe 'DELETE #destroy' do
-    context 'when signed in' do
-      let(:user) { Fabricate(:user, password: 'petsmoldoggos') }
-
-      before do
-        sign_in user, scope: :user
-      end
-
-      context 'with correct password' do
-        before do
-          delete :destroy, params: { form_delete_confirmation: { password: 'petsmoldoggos' } }
-        end
-
-        it 'removes user record and redirects', :aggregate_failures, :inline_jobs do
-          expect(response).to redirect_to '/auth/sign_in'
-          expect(User.find_by(id: user.id)).to be_nil
-          expect(user.account.reload).to be_suspended
-          expect(CanonicalEmailBlock.block?(user.email)).to be false
-        end
-
-        context 'when suspended' do
-          let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
-
-          it 'returns http forbidden' do
-            expect(response).to have_http_status(403)
-          end
-        end
-      end
-
-      context 'with incorrect password' do
-        before do
-          delete :destroy, params: { form_delete_confirmation: { password: 'blaze420' } }
-        end
-
-        it 'redirects back to confirmation page' do
-          expect(response).to redirect_to settings_delete_path
-        end
-      end
-    end
-
-    context 'when not signed in' do
-      it 'redirects' do
-        delete :destroy
-        expect(response).to redirect_to '/auth/sign_in'
-      end
-    end
-  end
-end
diff --git a/spec/controllers/settings/featured_tags_controller_spec.rb b/spec/controllers/settings/featured_tags_controller_spec.rb
deleted file mode 100644
index f414e818f5..0000000000
--- a/spec/controllers/settings/featured_tags_controller_spec.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Settings::FeaturedTagsController do
-  render_views
-
-  context 'when user is not signed in' do
-    subject { post :create }
-
-    it { is_expected.to redirect_to new_user_session_path }
-  end
-
-  context 'when user is signed in' do
-    let(:user) { Fabricate(:user, password: '12345678') }
-
-    before { sign_in user, scope: :user }
-
-    describe 'POST #create' do
-      subject { post :create, params: { featured_tag: params } }
-
-      context 'when parameter is valid' do
-        let(:params) { { name: 'test' } }
-
-        it 'creates featured tag' do
-          expect { subject }.to change { user.account.featured_tags.count }.by(1)
-        end
-      end
-
-      context 'when parameter is invalid' do
-        let(:params) { { name: 'test, #foo !bleh' } }
-
-        it 'renders new' do
-          expect(subject).to render_template :index
-        end
-      end
-    end
-
-    describe 'GET to #index' do
-      let(:tag) { Fabricate(:tag) }
-
-      before do
-        status = Fabricate :status, account: user.account
-        status.tags << tag
-      end
-
-      it 'responds with success' do
-        get :index
-
-        expect(response).to have_http_status(200)
-        expect(response.body).to include(
-          settings_featured_tags_path(featured_tag: { name: tag.name })
-        )
-      end
-    end
-
-    describe 'DELETE to #destroy' do
-      let(:featured_tag) { Fabricate(:featured_tag, account: user.account) }
-
-      it 'removes the featured tag' do
-        delete :destroy, params: { id: featured_tag.id }
-
-        expect(response).to redirect_to(settings_featured_tags_path)
-        expect { featured_tag.reload }.to raise_error(ActiveRecord::RecordNotFound)
-      end
-    end
-  end
-end
diff --git a/spec/controllers/settings/migration/redirects_controller_spec.rb b/spec/controllers/settings/migration/redirects_controller_spec.rb
deleted file mode 100644
index d853fe8ae6..0000000000
--- a/spec/controllers/settings/migration/redirects_controller_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Settings::Migration::RedirectsController do
-  render_views
-
-  let!(:user) { Fabricate(:user, password: 'testtest') }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #new' do
-    before do
-      get :new
-    end
-
-    it 'returns http success with private cache control headers', :aggregate_failures do
-      expect(response).to have_http_status(200)
-      expect(response.headers['Cache-Control']).to include('private, no-store')
-    end
-  end
-
-  describe 'POST #create' do
-    context 'with valid params' do
-      before { stub_resolver }
-
-      it 'redirects to the settings migration path' do
-        post :create, params: { form_redirect: { acct: 'new@host.com', current_password: 'testtest' } }
-
-        expect(response).to redirect_to(settings_migration_path)
-      end
-    end
-
-    context 'with non valid params' do
-      it 'returns success and renders the new page' do
-        post :create, params: { form_redirect: { acct: '' } }
-
-        expect(response).to have_http_status(200)
-        expect(response).to render_template(:new)
-      end
-    end
-  end
-
-  describe 'DELETE #destroy' do
-    let(:account) { Fabricate(:account) }
-
-    before do
-      user.account.update(moved_to_account_id: account.id)
-    end
-
-    it 'resets the account and sends an update' do
-      delete :destroy
-
-      expect(response).to redirect_to(settings_migration_path)
-      expect(user.account.reload.moved_to_account).to be_nil
-    end
-  end
-
-  private
-
-  def stub_resolver
-    resolver = instance_double(ResolveAccountService, call: Fabricate(:account))
-    allow(ResolveAccountService).to receive(:new).and_return(resolver)
-  end
-end
diff --git a/spec/controllers/settings/pictures_controller_spec.rb b/spec/controllers/settings/pictures_controller_spec.rb
deleted file mode 100644
index 683d231ed1..0000000000
--- a/spec/controllers/settings/pictures_controller_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Settings::PicturesController do
-  render_views
-
-  let!(:user) { Fabricate(:user) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'DELETE #destroy' do
-    context 'with invalid picture id' do
-      it 'returns http bad request' do
-        delete :destroy, params: { id: 'invalid' }
-        expect(response).to have_http_status(400)
-      end
-    end
-
-    context 'with valid picture id' do
-      context 'when account updates correctly' do
-        let(:service) { instance_double(UpdateAccountService, call: true) }
-
-        before do
-          allow(UpdateAccountService).to receive(:new).and_return(service)
-        end
-
-        it 'updates the account' do
-          delete :destroy, params: { id: 'avatar' }
-          expect(response).to redirect_to(settings_profile_path)
-          expect(response).to have_http_status(303)
-          expect(service).to have_received(:call).with(user.account, { 'avatar' => nil, 'avatar_remote_url' => '' })
-        end
-      end
-
-      context 'when account cannot update' do
-        let(:service) { instance_double(UpdateAccountService, call: false) }
-
-        before do
-          allow(UpdateAccountService).to receive(:new).and_return(service)
-        end
-
-        it 'redirects to profile' do
-          delete :destroy, params: { id: 'avatar' }
-          expect(response).to redirect_to(settings_profile_path)
-        end
-      end
-    end
-  end
-end
diff --git a/spec/controllers/settings/privacy_controller_spec.rb b/spec/controllers/settings/privacy_controller_spec.rb
deleted file mode 100644
index 59fd342199..0000000000
--- a/spec/controllers/settings/privacy_controller_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Settings::PrivacyController do
-  render_views
-
-  let!(:user) { Fabricate(:user) }
-  let(:account) { user.account }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #show' do
-    before do
-      get :show
-    end
-
-    it 'returns http success with private cache control headers', :aggregate_failures do
-      expect(response)
-        .to have_http_status(200)
-        .and have_attributes(
-          headers: include(
-            'Cache-Control' => 'private, no-store'
-          )
-        )
-    end
-  end
-
-  describe 'PUT #update' do
-    context 'when update succeeds' do
-      before do
-        allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
-      end
-
-      it 'updates the user profile' do
-        put :update, params: { account: { discoverable: '1', settings: { indexable: '1' } } }
-
-        expect(account.reload.discoverable)
-          .to be(true)
-
-        expect(response)
-          .to redirect_to(settings_privacy_path)
-
-        expect(ActivityPub::UpdateDistributionWorker)
-          .to have_received(:perform_async).with(account.id)
-      end
-    end
-
-    context 'when update fails' do
-      before do
-        allow(UpdateAccountService).to receive(:new).and_return(failing_update_service)
-        allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
-      end
-
-      it 'updates the user profile' do
-        put :update, params: { account: { discoverable: '1', settings: { indexable: '1' } } }
-
-        expect(response)
-          .to render_template(:show)
-
-        expect(ActivityPub::UpdateDistributionWorker)
-          .to_not have_received(:perform_async)
-      end
-
-      private
-
-      def failing_update_service
-        instance_double(UpdateAccountService, call: false)
-      end
-    end
-  end
-end
diff --git a/spec/controllers/settings/sessions_controller_spec.rb b/spec/controllers/settings/sessions_controller_spec.rb
deleted file mode 100644
index c098af7485..0000000000
--- a/spec/controllers/settings/sessions_controller_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Settings::SessionsController do
-  render_views
-
-  let(:user) { Fabricate(:user) }
-  let(:session_activation) { Fabricate(:session_activation, user: user) }
-
-  before { sign_in user, scope: :user }
-
-  describe 'DELETE #destroy' do
-    subject { delete :destroy, params: { id: id } }
-
-    context 'when session activation exists' do
-      let(:id) { session_activation.id }
-
-      it 'destroys session activation' do
-        expect(subject).to redirect_to edit_user_registration_path
-        expect(SessionActivation.find_by(id: id)).to be_nil
-      end
-    end
-
-    context 'when session activation does not exist' do
-      let(:id) { session_activation.id + 1000 }
-
-      it 'destroys session activation' do
-        expect(subject).to have_http_status 404
-      end
-    end
-  end
-end
diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
index 224310b7ef..45c5e77323 100644
--- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
@@ -5,14 +5,14 @@ require 'rails_helper'
 RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do
   render_views
 
-  shared_examples 'renders :new' do
-    it 'renders the new view' do
+  shared_examples 'renders expected page' do
+    it 'renders the new view with QR code' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(response).to render_template(:new)
       expect(response.body)
         .to include(qr_code_markup)
+        .and include(I18n.t('settings.two_factor_authentication'))
     end
 
     def qr_code_markup
@@ -34,7 +34,7 @@ RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do
             get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
           end
 
-          include_examples 'renders :new'
+          include_examples 'renders expected page'
         end
 
         it 'redirects if a new otp_secret has not been set in the session' do
@@ -66,10 +66,13 @@ RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do
             expect { post_create_with_options }
               .to change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview'
 
-            expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
-            expect(response).to have_http_status(200)
-            expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
-            expect(response.body).to include(*otp_backup_codes)
+            expect(flash[:notice])
+              .to eq(I18n.t('two_factor_authentication.enabled_success'))
+            expect(response)
+              .to have_http_status(200)
+            expect(response.body)
+              .to include(*otp_backup_codes)
+              .and include(I18n.t('settings.two_factor_authentication'))
           end
         end
 
@@ -86,10 +89,12 @@ RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do
 
           it 'renders page with error message' do
             subject
-            expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?'
+
+            expect(response.body)
+              .to include(I18n.t('otp_authentication.wrong_code'))
           end
 
-          include_examples 'renders :new'
+          include_examples 'renders expected page'
         end
 
         private
@@ -116,18 +121,4 @@ RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do
       end
     end
   end
-
-  context 'when not signed in' do
-    it 'redirects on POST to create' do
-      post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
-
-      expect(response).to redirect_to('/auth/sign_in')
-    end
-
-    it 'redirects on GET to new' do
-      get :new
-
-      expect(response).to redirect_to('/auth/sign_in')
-    end
-  end
 end
diff --git a/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
deleted file mode 100644
index 0defc52cde..0000000000
--- a/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Settings::TwoFactorAuthentication::RecoveryCodesController do
-  render_views
-
-  describe 'POST #create' do
-    it 'updates the codes and shows them on a view when signed in' do
-      user = Fabricate(:user)
-      otp_backup_codes = user.generate_otp_backup_codes!
-      allow(user).to receive(:generate_otp_backup_codes!).and_return(otp_backup_codes)
-      allow(controller).to receive(:current_user).and_return(user)
-
-      sign_in user, scope: :user
-      post :create, session: { challenge_passed_at: Time.now.utc }
-
-      expect(flash[:notice]).to eq 'Recovery codes successfully regenerated'
-      expect(response).to have_http_status(200)
-      expect(response).to render_template(:index)
-      expect(response.body)
-        .to include(*otp_backup_codes)
-    end
-
-    it 'redirects when not signed in' do
-      post :create
-      expect(response).to redirect_to '/auth/sign_in'
-    end
-  end
-end
diff --git a/spec/controllers/settings/two_factor_authentication_methods_controller_spec.rb b/spec/controllers/settings/two_factor_authentication_methods_controller_spec.rb
deleted file mode 100644
index c55f113d4d..0000000000
--- a/spec/controllers/settings/two_factor_authentication_methods_controller_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Settings::TwoFactorAuthenticationMethodsController do
-  render_views
-
-  context 'when not signed in' do
-    describe 'GET to #index' do
-      it 'redirects' do
-        get :index
-
-        expect(response).to redirect_to '/auth/sign_in'
-      end
-    end
-  end
-
-  context 'when signed in' do
-    let(:user) { Fabricate(:user) }
-
-    before do
-      sign_in user, scope: :user
-    end
-
-    describe 'GET #index' do
-      describe 'when user has enabled otp' do
-        before do
-          user.update(otp_required_for_login: true)
-          get :index
-        end
-
-        it 'returns http success with private cache control headers', :aggregate_failures do
-          expect(response).to have_http_status(200)
-          expect(response.headers['Cache-Control']).to include('private, no-store')
-        end
-      end
-
-      describe 'when user has not enabled otp' do
-        before do
-          user.update(otp_required_for_login: false)
-          get :index
-        end
-
-        it 'redirects to enable otp' do
-          expect(response).to redirect_to(settings_otp_authentication_path)
-        end
-      end
-    end
-
-    describe 'POST to #disable' do
-      before do
-        user.update(otp_required_for_login: true)
-      end
-
-      context 'when user has not passed challenge' do
-        it 'renders challenge page' do
-          post :disable
-
-          expect(response).to have_http_status(200)
-          expect(response).to render_template('auth/challenges/new')
-        end
-      end
-
-      context 'when user has passed challenge' do
-        before do
-          mailer = instance_double(ApplicationMailer::MessageDelivery, deliver_later!: true)
-          allow(UserMailer).to receive(:two_factor_disabled).with(user).and_return(mailer)
-        end
-
-        it 'redirects to settings page' do
-          post :disable, session: { challenge_passed_at: 10.minutes.ago }
-
-          expect(UserMailer).to have_received(:two_factor_disabled).with(user)
-          expect(response).to redirect_to(settings_otp_authentication_path)
-        end
-      end
-    end
-  end
-end
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
deleted file mode 100644
index e589693b17..0000000000
--- a/spec/controllers/statuses_controller_spec.rb
+++ /dev/null
@@ -1,480 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe StatusesController do
-  render_views
-
-  describe 'GET #show' do
-    let(:account) { Fabricate(:account) }
-    let(:status)  { Fabricate(:status, account: account) }
-
-    context 'when status is public' do
-      before do
-        get :show, params: { account_username: status.account.username, id: status.id, format: format }
-      end
-
-      context 'with HTML' do
-        let(:format) { 'html' }
-
-        it 'renders status successfully', :aggregate_failures do
-          expect(response)
-            .to have_http_status(200)
-            .and render_template(:show)
-          expect(response.headers).to include(
-            'Vary' => 'Accept, Accept-Language, Cookie',
-            'Cache-Control' => include('public'),
-            'Link' => include('activity+json')
-          )
-          expect(response.body).to include status.text
-        end
-      end
-
-      context 'with JSON' do
-        let(:format) { 'json' }
-
-        it 'renders ActivityPub Note object successfully', :aggregate_failures do
-          expect(response)
-            .to have_http_status(200)
-            .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
-
-          expect(response.headers).to include(
-            'Content-Type' => include('application/activity+json'),
-            'Link' => include('activity+json')
-          )
-          expect(response.parsed_body)
-            .to include(content: include(status.text))
-        end
-      end
-    end
-
-    context 'when status is private' do
-      let(:status) { Fabricate(:status, account: account, visibility: :private) }
-
-      before do
-        get :show, params: { account_username: status.account.username, id: status.id, format: format }
-      end
-
-      context 'with JSON' do
-        let(:format) { 'json' }
-
-        it 'returns http not found' do
-          expect(response).to have_http_status(404)
-        end
-      end
-
-      context 'with HTML' do
-        let(:format) { 'html' }
-
-        it 'returns http not found' do
-          expect(response).to have_http_status(404)
-        end
-      end
-    end
-
-    context 'when status is direct' do
-      let(:status) { Fabricate(:status, account: account, visibility: :direct) }
-
-      before do
-        get :show, params: { account_username: status.account.username, id: status.id, format: format }
-      end
-
-      context 'with JSON' do
-        let(:format) { 'json' }
-
-        it 'returns http not found' do
-          expect(response).to have_http_status(404)
-        end
-      end
-
-      context 'with HTML' do
-        let(:format) { 'html' }
-
-        it 'returns http not found' do
-          expect(response).to have_http_status(404)
-        end
-      end
-    end
-
-    context 'when signed-in' do
-      let(:user) { Fabricate(:user) }
-
-      before do
-        sign_in(user)
-      end
-
-      context 'when status is public' do
-        before do
-          get :show, params: { account_username: status.account.username, id: status.id, format: format }
-        end
-
-        context 'with HTML' do
-          let(:format) { 'html' }
-
-          it 'renders status successfully', :aggregate_failures do
-            expect(response)
-              .to have_http_status(200)
-              .and render_template(:show)
-            expect(response.headers).to include(
-              'Vary' => 'Accept, Accept-Language, Cookie',
-              'Cache-Control' => include('private'),
-              'Link' => include('activity+json')
-            )
-            expect(response.body).to include status.text
-          end
-        end
-
-        context 'with JSON' do
-          let(:format) { 'json' }
-
-          it 'renders ActivityPub Note object successfully', :aggregate_failures do
-            expect(response)
-              .to have_http_status(200)
-            expect(response.headers).to include(
-              'Vary' => 'Accept, Accept-Language, Cookie',
-              'Cache-Control' => include('private'),
-              'Content-Type' => include('application/activity+json'),
-              'Link' => include('activity+json')
-            )
-            expect(response.parsed_body)
-              .to include(content: include(status.text))
-          end
-        end
-      end
-
-      context 'when status is private' do
-        let(:status) { Fabricate(:status, account: account, visibility: :private) }
-
-        context 'when user is authorized to see it' do
-          before do
-            user.account.follow!(account)
-            get :show, params: { account_username: status.account.username, id: status.id, format: format }
-          end
-
-          context 'with HTML' do
-            let(:format) { 'html' }
-
-            it 'renders status successfully', :aggregate_failures do
-              expect(response)
-                .to have_http_status(200)
-                .and render_template(:show)
-
-              expect(response.headers).to include(
-                'Vary' => 'Accept, Accept-Language, Cookie',
-                'Cache-Control' => include('private'),
-                'Link' => include('activity+json')
-              )
-              expect(response.body).to include status.text
-            end
-          end
-
-          context 'with JSON' do
-            let(:format) { 'json' }
-
-            it 'renders ActivityPub Note object successfully', :aggregate_failures do
-              expect(response)
-                .to have_http_status(200)
-              expect(response.headers).to include(
-                'Vary' => 'Accept, Accept-Language, Cookie',
-                'Cache-Control' => include('private'),
-                'Content-Type' => include('application/activity+json'),
-                'Link' => include('activity+json')
-              )
-              expect(response.parsed_body)
-                .to include(content: include(status.text))
-            end
-          end
-        end
-
-        context 'when user is not authorized to see it' do
-          before do
-            get :show, params: { account_username: status.account.username, id: status.id, format: format }
-          end
-
-          context 'with JSON' do
-            let(:format) { 'json' }
-
-            it 'returns http not found' do
-              expect(response).to have_http_status(404)
-            end
-          end
-
-          context 'with HTML' do
-            let(:format) { 'html' }
-
-            it 'returns http not found' do
-              expect(response).to have_http_status(404)
-            end
-          end
-        end
-      end
-
-      context 'when status is direct' do
-        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
-
-        context 'when user is authorized to see it' do
-          before do
-            Fabricate(:mention, account: user.account, status: status)
-            get :show, params: { account_username: status.account.username, id: status.id, format: format }
-          end
-
-          context 'with HTML' do
-            let(:format) { 'html' }
-
-            it 'renders status successfully', :aggregate_failures do
-              expect(response)
-                .to have_http_status(200)
-                .and render_template(:show)
-              expect(response.headers).to include(
-                'Vary' => 'Accept, Accept-Language, Cookie',
-                'Cache-Control' => include('private'),
-                'Link' => include('activity+json')
-              )
-              expect(response.body).to include status.text
-            end
-          end
-
-          context 'with JSON' do
-            let(:format) { 'json' }
-
-            it 'renders ActivityPub Note object successfully' do
-              expect(response)
-                .to have_http_status(200)
-              expect(response.headers).to include(
-                'Vary' => 'Accept, Accept-Language, Cookie',
-                'Cache-Control' => include('private'),
-                'Content-Type' => include('application/activity+json'),
-                'Link' => include('activity+json')
-              )
-              expect(response.parsed_body)
-                .to include(content: include(status.text))
-            end
-          end
-        end
-
-        context 'when user is not authorized to see it' do
-          before do
-            get :show, params: { account_username: status.account.username, id: status.id, format: format }
-          end
-
-          context 'with JSON' do
-            let(:format) { 'json' }
-
-            it 'returns http not found' do
-              expect(response).to have_http_status(404)
-            end
-          end
-
-          context 'with HTML' do
-            let(:format) { 'html' }
-
-            it 'returns http not found' do
-              expect(response).to have_http_status(404)
-            end
-          end
-        end
-      end
-    end
-
-    context 'with signature' do
-      let(:remote_account) { Fabricate(:account, domain: 'example.com') }
-
-      before do
-        allow(controller).to receive(:signed_request_actor).and_return(remote_account)
-      end
-
-      context 'when account blocks account' do
-        before do
-          account.block!(remote_account)
-          get :show, params: { account_username: status.account.username, id: status.id }
-        end
-
-        it 'returns http not found' do
-          expect(response).to have_http_status(404)
-        end
-      end
-
-      context 'when account domain blocks account' do
-        before do
-          account.block_domain!(remote_account.domain)
-          get :show, params: { account_username: status.account.username, id: status.id }
-        end
-
-        it 'returns http not found' do
-          expect(response).to have_http_status(404)
-        end
-      end
-
-      context 'when status is public' do
-        before do
-          get :show, params: { account_username: status.account.username, id: status.id, format: format }
-        end
-
-        context 'with HTML' do
-          let(:format) { 'html' }
-
-          it 'renders status successfully', :aggregate_failures do
-            expect(response)
-              .to have_http_status(200)
-              .and render_template(:show)
-            expect(response.headers).to include(
-              'Vary' => 'Accept, Accept-Language, Cookie',
-              'Cache-Control' => include('private'),
-              'Link' => include('activity+json')
-            )
-            expect(response.body).to include status.text
-          end
-        end
-
-        context 'with JSON' do
-          let(:format) { 'json' }
-
-          it 'renders ActivityPub Note object successfully', :aggregate_failures do
-            expect(response)
-              .to have_http_status(200)
-              .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
-            expect(response.headers).to include(
-              'Content-Type' => include('application/activity+json'),
-              'Link' => include('activity+json')
-            )
-            expect(response.parsed_body)
-              .to include(content: include(status.text))
-          end
-        end
-      end
-
-      context 'when status is private' do
-        let(:status) { Fabricate(:status, account: account, visibility: :private) }
-
-        context 'when user is authorized to see it' do
-          before do
-            remote_account.follow!(account)
-            get :show, params: { account_username: status.account.username, id: status.id, format: format }
-          end
-
-          context 'with HTML' do
-            let(:format) { 'html' }
-
-            it 'renders status successfully', :aggregate_failures do
-              expect(response)
-                .to have_http_status(200)
-                .and render_template(:show)
-              expect(response.headers).to include(
-                'Vary' => 'Accept, Accept-Language, Cookie',
-                'Cache-Control' => include('private'),
-                'Link' => include('activity+json')
-              )
-              expect(response.body).to include status.text
-            end
-          end
-
-          context 'with JSON' do
-            let(:format) { 'json' }
-
-            it 'renders ActivityPub Note object successfully' do
-              expect(response)
-                .to have_http_status(200)
-              expect(response.headers).to include(
-                'Vary' => 'Accept, Accept-Language, Cookie',
-                'Cache-Control' => include('private'),
-                'Content-Type' => include('application/activity+json'),
-                'Link' => include('activity+json')
-              )
-
-              expect(response.parsed_body)
-                .to include(content: include(status.text))
-            end
-          end
-        end
-
-        context 'when user is not authorized to see it' do
-          before do
-            get :show, params: { account_username: status.account.username, id: status.id, format: format }
-          end
-
-          context 'with JSON' do
-            let(:format) { 'json' }
-
-            it 'returns http not found' do
-              expect(response).to have_http_status(404)
-            end
-          end
-
-          context 'with HTML' do
-            let(:format) { 'html' }
-
-            it 'returns http not found' do
-              expect(response).to have_http_status(404)
-            end
-          end
-        end
-      end
-
-      context 'when status is direct' do
-        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
-
-        context 'when user is authorized to see it' do
-          before do
-            Fabricate(:mention, account: remote_account, status: status)
-            get :show, params: { account_username: status.account.username, id: status.id, format: format }
-          end
-
-          context 'with HTML' do
-            let(:format) { 'html' }
-
-            it 'renders status successfully', :aggregate_failures do
-              expect(response)
-                .to have_http_status(200)
-                .and render_template(:show)
-              expect(response.headers).to include(
-                'Vary' => 'Accept, Accept-Language, Cookie',
-                'Cache-Control' => include('private'),
-                'Link' => include('activity+json')
-              )
-              expect(response.body).to include status.text
-            end
-          end
-
-          context 'with JSON' do
-            let(:format) { 'json' }
-
-            it 'renders ActivityPub Note object', :aggregate_failures do
-              expect(response)
-                .to have_http_status(200)
-              expect(response.headers).to include(
-                'Vary' => 'Accept, Accept-Language, Cookie',
-                'Cache-Control' => include('private'),
-                'Content-Type' => include('application/activity+json'),
-                'Link' => include('activity+json')
-              )
-              expect(response.parsed_body)
-                .to include(content: include(status.text))
-            end
-          end
-        end
-
-        context 'when user is not authorized to see it' do
-          before do
-            get :show, params: { account_username: status.account.username, id: status.id, format: format }
-          end
-
-          context 'with JSON' do
-            let(:format) { 'json' }
-
-            it 'returns http not found' do
-              expect(response).to have_http_status(404)
-            end
-          end
-
-          context 'with HTML' do
-            let(:format) { 'html' }
-
-            it 'returns http not found' do
-              expect(response).to have_http_status(404)
-            end
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/spec/fabricators/fasp/debug_callback_fabricator.rb b/spec/fabricators/fasp/debug_callback_fabricator.rb
new file mode 100644
index 0000000000..28c1c00be8
--- /dev/null
+++ b/spec/fabricators/fasp/debug_callback_fabricator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Fabricator(:fasp_debug_callback, from: 'Fasp::DebugCallback') do
+  fasp_provider
+  ip            '127.0.0.234'
+  request_body  'MyText'
+end
diff --git a/spec/fabricators/fasp/provider_fabricator.rb b/spec/fabricators/fasp/provider_fabricator.rb
new file mode 100644
index 0000000000..fd7867402a
--- /dev/null
+++ b/spec/fabricators/fasp/provider_fabricator.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+Fabricator(:fasp_provider, from: 'Fasp::Provider') do
+  name                    { Faker::App.name }
+  base_url                { Faker::Internet.unique.url }
+  sign_in_url             { Faker::Internet.url }
+  remote_identifier       'MyString'
+  provider_public_key_pem "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAh2ldXsaej2MXj0DHdCx7XibSo66uKlrLfJ5J6hte1Gk=\n-----END PUBLIC KEY-----\n"
+  server_private_key_pem  "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEICDjlajhVb8XfzyTchQWKraMKwtQW+r4opoAg7V3kw1Q\n-----END PRIVATE KEY-----\n"
+  capabilities            []
+end
+
+Fabricator(:confirmed_fasp, from: :fasp_provider) do
+  confirmed    true
+  capabilities [
+    { id: 'callback', version: '0.1' },
+    { id: 'data_sharing', version: '0.1' },
+  ]
+end
+
+Fabricator(:debug_fasp, from: :fasp_provider) do
+  confirmed    true
+  capabilities [
+    { id: 'callback', version: '0.1', enabled: true },
+  ]
+
+  after_build do |fasp|
+    # Prevent fabrication from attempting an HTTP call to the provider
+    def fasp.update_remote_capabilities = true
+  end
+end
diff --git a/spec/fabricators/import_fabricator.rb b/spec/fabricators/import_fabricator.rb
deleted file mode 100644
index 4951bb9a4d..0000000000
--- a/spec/fabricators/import_fabricator.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:import) do
-  account
-  type :following
-  data { attachment_fixture('imports.txt') }
-end
diff --git a/spec/fabricators/terms_of_service_fabricator.rb b/spec/fabricators/terms_of_service_fabricator.rb
index 2b0cfabcfb..9b437c4d4b 100644
--- a/spec/fabricators/terms_of_service_fabricator.rb
+++ b/spec/fabricators/terms_of_service_fabricator.rb
@@ -5,4 +5,5 @@ Fabricator(:terms_of_service) do
   changelog { Faker::Lorem.paragraph }
   published_at { Time.zone.now }
   notification_sent_at { Time.zone.now }
+  effective_date { Faker::Date.unique.forward }
 end
diff --git a/spec/fixtures/files/4096x4097.png b/spec/fixtures/files/4096x4097.png
deleted file mode 100644
index d1110cc2df..0000000000
Binary files a/spec/fixtures/files/4096x4097.png and /dev/null differ
diff --git a/spec/flatware_helper.rb b/spec/flatware_helper.rb
index 57a7c1f56a..a1bcb62340 100644
--- a/spec/flatware_helper.rb
+++ b/spec/flatware_helper.rb
@@ -3,7 +3,7 @@
 if defined?(Flatware)
   Flatware.configure do |config|
     config.after_fork do |test_env_number|
-      unless ENV.fetch('DISABLE_SIMPLECOV', nil) == 'true'
+      if ENV.fetch('COVERAGE', false)
         require 'simplecov'
         SimpleCov.at_fork.call(test_env_number) # Combines parallel coverage results
       end
diff --git a/spec/helpers/admin/trends/statuses_helper_spec.rb b/spec/helpers/admin/trends/statuses_helper_spec.rb
index fa5c337e97..6abc4569b4 100644
--- a/spec/helpers/admin/trends/statuses_helper_spec.rb
+++ b/spec/helpers/admin/trends/statuses_helper_spec.rb
@@ -28,6 +28,19 @@ RSpec.describe Admin::Trends::StatusesHelper do
       end
     end
 
+    context 'with a remote status that has excessive attributes' do
+      let(:attr_limit) { Nokogiri::Gumbo::DEFAULT_MAX_ATTRIBUTES * 2 }
+      let(:html) { "<html><body #{(1..attr_limit).map { |x| "attr-#{x}" }.join(' ')}><p>text</p></body></html>" }
+
+      let(:status) { Fabricate.build(:status, uri: 'https://host.example', text: html) }
+
+      it 'renders a correct preview text' do
+        result = helper.one_line_preview(status)
+
+        expect(result).to eq ''
+      end
+    end
+
     context 'with a status that has empty text' do
       let(:status) { Fabricate.build(:status, text: '') }
 
diff --git a/spec/lib/account_reach_finder_spec.rb b/spec/lib/account_reach_finder_spec.rb
index 0c1d92b2da..ed16c07c22 100644
--- a/spec/lib/account_reach_finder_spec.rb
+++ b/spec/lib/account_reach_finder_spec.rb
@@ -13,13 +13,28 @@ RSpec.describe AccountReachFinder do
   let(:ap_mentioned_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-3', domain: 'example.com') }
   let(:ap_mentioned_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.org/inbox-4', domain: 'example.org') }
 
+  let(:ap_followed_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-5', domain: 'example.com') }
+  let(:ap_followed_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-6', domain: 'example.org') }
+
+  let(:ap_requested_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-7', domain: 'example.com') }
+  let(:ap_requested_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-8', domain: 'example.org') }
+
   let(:unrelated_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/unrelated-inbox', domain: 'example.com') }
+  let(:old_followed_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/old-followed-inbox', domain: 'example.com') }
 
   before do
+    travel_to(2.months.ago) { account.follow!(old_followed_account) }
+
     ap_follower_example_com.follow!(account)
     ap_follower_example_org.follow!(account)
     ap_follower_with_shared.follow!(account)
 
+    account.follow!(ap_followed_example_com)
+    account.follow!(ap_followed_example_org)
+
+    account.request_follow!(ap_requested_example_com)
+    account.request_follow!(ap_requested_example_org)
+
     Fabricate(:status, account: account).tap do |status|
       status.mentions << Mention.new(account: ap_follower_example_com)
       status.mentions << Mention.new(account: ap_mentioned_with_shared)
@@ -44,7 +59,10 @@ RSpec.describe AccountReachFinder do
       expect(subject)
         .to include(*follower_inbox_urls)
         .and include(*mentioned_account_inbox_urls)
+        .and include(*recently_followed_inbox_urls)
+        .and include(*recently_requested_inbox_urls)
         .and not_include(unrelated_account.preferred_inbox_url)
+        .and not_include(old_followed_account.preferred_inbox_url)
     end
 
     def follower_inbox_urls
@@ -56,5 +74,15 @@ RSpec.describe AccountReachFinder do
       [ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org]
         .map(&:preferred_inbox_url)
     end
+
+    def recently_followed_inbox_urls
+      [ap_followed_example_com, ap_followed_example_org]
+        .map(&:preferred_inbox_url)
+    end
+
+    def recently_requested_inbox_urls
+      [ap_requested_example_com, ap_requested_example_org]
+        .map(&:preferred_inbox_url)
+    end
   end
 end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index f5528f13a7..108111c06b 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -48,10 +48,16 @@ RSpec.describe ActivityPub::Activity::Create do
         content: '@bob lorem ipsum',
         published: 1.hour.ago.utc.iso8601,
         updated: 1.hour.ago.utc.iso8601,
-        tag: {
-          type: 'Mention',
-          href: ActivityPub::TagManager.instance.uri_for(follower),
-        },
+        tag: [
+          {
+            type: 'Mention',
+            href: ActivityPub::TagManager.instance.uri_for(follower),
+          },
+          {
+            type: 'Mention',
+            href: ActivityPub::TagManager.instance.uri_for(follower),
+          },
+        ],
       }
     end
 
diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb
index d4e6df09ce..87f8d568be 100644
--- a/spec/lib/activitypub/tag_manager_spec.rb
+++ b/spec/lib/activitypub/tag_manager_spec.rb
@@ -287,6 +287,23 @@ RSpec.describe ActivityPub::TagManager do
     end
   end
 
+  describe '#uris_to_local_accounts' do
+    it 'returns the expected local accounts' do
+      account = Fabricate(:account)
+      expect(subject.uris_to_local_accounts([subject.uri_for(account), instance_actor_url])).to contain_exactly(account, Account.representative)
+    end
+
+    it 'does not return remote accounts' do
+      account = Fabricate(:account, uri: 'https://example.com/123', domain: 'example.com')
+      expect(subject.uris_to_local_accounts([subject.uri_for(account)])).to be_empty
+    end
+
+    it 'does not return an account for a local post' do
+      status = Fabricate(:status)
+      expect(subject.uris_to_local_accounts([subject.uri_for(status)])).to be_empty
+    end
+  end
+
   describe '#uri_to_resource' do
     it 'returns the local account' do
       account = Fabricate(:account)
diff --git a/spec/lib/fasp/request_spec.rb b/spec/lib/fasp/request_spec.rb
new file mode 100644
index 0000000000..5d81c09722
--- /dev/null
+++ b/spec/lib/fasp/request_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'securerandom'
+
+RSpec.describe Fasp::Request do
+  include ProviderRequestHelper
+
+  subject { described_class.new(provider) }
+
+  let(:provider) do
+    Fabricate(:fasp_provider, base_url: 'https://reqprov.example.com/fasp')
+  end
+
+  shared_examples 'a provider request' do |method|
+    context 'when the response is signed by the provider' do
+      before do
+        stub_provider_request(provider, method:, path: '/test_path')
+      end
+
+      it "performs a signed #{method.to_s.upcase} request relative to the base_path of the fasp" do
+        subject.send(method, '/test_path')
+
+        expect(WebMock).to have_requested(method, 'https://reqprov.example.com/fasp/test_path')
+          .with(headers: {
+            'Signature' => /.+/,
+            'Signature-Input' => /.+/,
+          })
+      end
+    end
+
+    context 'when the response is not signed' do
+      before do
+        stub_request(method, 'https://reqprov.example.com/fasp/test_path')
+          .to_return(status: 200)
+      end
+
+      it 'raises an error' do
+        expect do
+          subject.send(method, '/test_path')
+        end.to raise_error(Mastodon::SignatureVerificationError)
+      end
+    end
+  end
+
+  describe '#get' do
+    include_examples 'a provider request', :get
+  end
+
+  describe '#post' do
+    include_examples 'a provider request', :post
+  end
+
+  describe '#delete' do
+    include_examples 'a provider request', :delete
+  end
+end
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index ad7913c758..b0b7505c8c 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -233,6 +233,39 @@ RSpec.describe FeedManager do
       end
     end
 
+    context 'with list feed' do
+      let(:list) { Fabricate(:list, account: bob) }
+
+      before do
+        bob.follow!(alice)
+        list.list_accounts.create!(account: alice)
+      end
+
+      it "returns false for followee's status" do
+        status = Fabricate(:status, text: 'Hello world', account: alice)
+
+        expect(subject.filter?(:list, status, list)).to be false
+      end
+
+      it 'returns false for reblog by followee' do
+        status = Fabricate(:status, text: 'Hello world', account: jeff)
+        reblog = Fabricate(:status, reblog: status, account: alice)
+
+        expect(subject.filter?(:list, reblog, list)).to be false
+      end
+    end
+
+    context 'with stl list feed' do
+      let(:antenna) { Fabricate(:antenna, account: bob, insert_feeds: true, list_id: list.id, stl: true) }
+      let(:list) { Fabricate(:list, account: bob) }
+
+      it "returns false for followee's status" do
+        status = Fabricate(:status, text: 'Hello world', account: alice, visibility: :unlisted)
+
+        expect(subject.filter?(:list, status, list, stl_home: true)).to be false
+      end
+    end
+
     context 'with mentions feed' do
       it 'returns true for status that mentions blocked account' do
         bob.block!(jeff)
diff --git a/spec/lib/mastodon/feature_spec.rb b/spec/lib/mastodon/feature_spec.rb
new file mode 100644
index 0000000000..f8236d8959
--- /dev/null
+++ b/spec/lib/mastodon/feature_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Mastodon::Feature do
+  describe '::testing_only_enabled?' do
+    subject { described_class.testing_only_enabled? }
+
+    it { is_expected.to be true }
+  end
+
+  describe '::unspecified_feature_enabled?' do
+    context 'when example is not tagged with a feature' do
+      subject { described_class.unspecified_feature_enabled? }
+
+      it { is_expected.to be false }
+    end
+
+    context 'when example is tagged with a feature', feature: 'unspecified_feature' do
+      subject { described_class.unspecified_feature_enabled? }
+
+      it { is_expected.to be true }
+    end
+  end
+end
diff --git a/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb b/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb
new file mode 100644
index 0000000000..eaab93772d
--- /dev/null
+++ b/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'prometheus_exporter'
+require 'prometheus_exporter/middleware'
+require 'mastodon/middleware/prometheus_queue_time'
+
+RSpec.describe Mastodon::Middleware::PrometheusQueueTime do
+  subject { described_class.new(app, client:) }
+
+  let(:app) do
+    proc { |_env| [200, {}, 'OK'] }
+  end
+  let(:client) do
+    instance_double(PrometheusExporter::Client, send_json: true)
+  end
+
+  describe '#call' do
+    let(:env) do
+      {
+        'HTTP_X_REQUEST_START' => "t=#{(Time.now.to_f * 1000).to_i}",
+      }
+    end
+
+    it 'reports a queue time to the client' do
+      subject.call(env)
+
+      expect(client).to have_received(:send_json)
+        .with(hash_including(queue_time: instance_of(Float)))
+    end
+  end
+end
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 533aa2e624..3f40e24c8b 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -317,4 +317,16 @@ RSpec.describe UserMailer do
         .and(have_body_text(I18n.t('user_mailer.terms_of_service_changed.changelog')))
     end
   end
+
+  describe '#announcement_published' do
+    let(:announcement) { Fabricate :announcement }
+    let(:mail) { described_class.announcement_published(receiver, announcement) }
+
+    it 'renders announcement_published mail' do
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('user_mailer.announcement_published.subject')))
+        .and(have_body_text(I18n.t('user_mailer.announcement_published.description', domain: Rails.configuration.x.local_domain)))
+    end
+  end
 end
diff --git a/spec/models/concerns/status/fetch_replies_concern_spec.rb b/spec/models/concerns/status/fetch_replies_concern_spec.rb
new file mode 100644
index 0000000000..e9c81d43b1
--- /dev/null
+++ b/spec/models/concerns/status/fetch_replies_concern_spec.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Status::FetchRepliesConcern do
+  ActiveRecord.verbose_query_logs = true
+
+  let!(:alice)  { Fabricate(:account, username: 'alice') }
+  let!(:bob)    { Fabricate(:account, username: 'bob', domain: 'other.com') }
+
+  let!(:account) { alice }
+  let!(:status_old) { Fabricate(:status, account: account, fetched_replies_at: 1.year.ago, created_at: 1.year.ago) }
+  let!(:status_fetched_recently) { Fabricate(:status, account: account, fetched_replies_at: 1.second.ago, created_at: 1.year.ago) }
+  let!(:status_created_recently) { Fabricate(:status, account: account, created_at: 1.second.ago) }
+  let!(:status_never_fetched) { Fabricate(:status, account: account, created_at: 1.year.ago) }
+
+  describe 'should_fetch_replies' do
+    let!(:statuses) { Status.should_fetch_replies.all }
+
+    context 'with a local status' do
+      it 'never fetches local replies' do
+        expect(statuses).to eq([])
+      end
+    end
+
+    context 'with a remote status' do
+      let(:account) { bob }
+
+      it 'fetches old statuses' do
+        expect(statuses).to include(status_old)
+      end
+
+      it 'fetches statuses that have never been fetched and weren\'t created recently' do
+        expect(statuses).to include(status_never_fetched)
+      end
+
+      it 'does not fetch statuses that were fetched recently' do
+        expect(statuses).to_not include(status_fetched_recently)
+      end
+
+      it 'does not fetch statuses that were created recently' do
+        expect(statuses).to_not include(status_created_recently)
+      end
+    end
+  end
+
+  describe 'should_not_fetch_replies' do
+    let!(:statuses) { Status.should_not_fetch_replies.all }
+
+    context 'with a local status' do
+      it 'does not fetch local statuses' do
+        expect(statuses).to include(status_old, status_never_fetched, status_fetched_recently, status_never_fetched)
+      end
+    end
+
+    context 'with a remote status' do
+      let(:account) { bob }
+
+      it 'fetches old statuses' do
+        expect(statuses).to_not include(status_old)
+      end
+
+      it 'fetches statuses that have never been fetched and weren\'t created recently' do
+        expect(statuses).to_not include(status_never_fetched)
+      end
+
+      it 'does not fetch statuses that were fetched recently' do
+        expect(statuses).to include(status_fetched_recently)
+      end
+
+      it 'does not fetch statuses that were created recently' do
+        expect(statuses).to include(status_created_recently)
+      end
+    end
+  end
+
+  describe 'unsubscribed' do
+    let!(:spike)  { Fabricate(:account, username: 'spike', domain: 'other.com') }
+    let!(:status) { Fabricate(:status, account: bob, updated_at: 1.day.ago) }
+
+    context 'when the status is from an account with only remote followers after last update' do
+      before do
+        Fabricate(:follow, account: spike, target_account: bob)
+      end
+
+      it 'shows the status as unsubscribed' do
+        expect(Status.unsubscribed).to eq([status])
+      end
+    end
+
+    context 'when the status is from an account with only remote followers before last update' do
+      before do
+        Fabricate(:follow, account: spike, target_account: bob, created_at: 2.days.ago)
+      end
+
+      it 'shows the status as unsubscribed' do
+        expect(Status.unsubscribed).to eq([status])
+      end
+    end
+
+    context 'when status is from account with local followers after last update' do
+      before do
+        Fabricate(:follow, account: alice, target_account: bob)
+      end
+
+      it 'shows the status as unsubscribed' do
+        expect(Status.unsubscribed).to eq([status])
+      end
+    end
+
+    context 'when status is from account with local followers before last update' do
+      before do
+        Fabricate(:follow, account: alice, target_account: bob, created_at: 2.days.ago)
+      end
+
+      it 'does not show the status as unsubscribed' do
+        expect(Status.unsubscribed).to eq([])
+      end
+    end
+
+    context 'when the status has no followers' do
+      it 'shows the status as unsubscribed' do
+        expect(Status.unsubscribed).to eq([status])
+      end
+    end
+  end
+end
diff --git a/spec/models/fasp/provider_spec.rb b/spec/models/fasp/provider_spec.rb
new file mode 100644
index 0000000000..52df4638fd
--- /dev/null
+++ b/spec/models/fasp/provider_spec.rb
@@ -0,0 +1,209 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Fasp::Provider do
+  include ProviderRequestHelper
+
+  describe '#capabilities' do
+    subject { described_class.new(confirmed: true, capabilities:) }
+
+    let(:capabilities) do
+      [
+        { 'id' => 'one', 'enabled' => false },
+        { 'id' => 'two' },
+      ]
+    end
+
+    it 'returns an array of `Fasp::Capability` objects' do
+      expect(subject.capabilities).to all(be_a(Fasp::Capability))
+    end
+  end
+
+  describe '#capabilities_attributes=' do
+    subject { described_class.new(confirmed: true) }
+
+    let(:capabilities_params) do
+      {
+        '0' => { 'id' => 'one', 'enabled' => '1' },
+        '1' => { 'id' => 'two', 'enabled' => '0' },
+        '2' => { 'id' => 'three' },
+      }
+    end
+
+    it 'sets capabilities from nested form style hash' do
+      subject.capabilities_attributes = capabilities_params
+
+      expect(subject).to be_capability('one')
+      expect(subject).to be_capability('two')
+      expect(subject).to be_capability('three')
+      expect(subject).to be_capability_enabled('one')
+      expect(subject).to_not be_capability_enabled('two')
+      expect(subject).to_not be_capability_enabled('three')
+    end
+  end
+
+  describe '#capability?' do
+    subject { described_class.new(confirmed:, capabilities:) }
+
+    let(:capabilities) do
+      [
+        { 'id' => 'one', 'enabled' => false },
+        { 'id' => 'two', 'enabled' => true },
+      ]
+    end
+
+    context 'when the provider is not confirmed' do
+      let(:confirmed) { false }
+
+      it 'always returns false' do
+        expect(subject.capability?('one')).to be false
+        expect(subject.capability?('two')).to be false
+      end
+    end
+
+    context 'when the provider is confirmed' do
+      let(:confirmed) { true }
+
+      it 'returns true for available and false for missing capabilities' do
+        expect(subject.capability?('one')).to be true
+        expect(subject.capability?('two')).to be true
+        expect(subject.capability?('three')).to be false
+      end
+    end
+  end
+
+  describe '#capability_enabled?' do
+    subject { described_class.new(confirmed:, capabilities:) }
+
+    let(:capabilities) do
+      [
+        { 'id' => 'one', 'enabled' => false },
+        { 'id' => 'two', 'enabled' => true },
+      ]
+    end
+
+    context 'when the provider is not confirmed' do
+      let(:confirmed) { false }
+
+      it 'always returns false' do
+        expect(subject).to_not be_capability_enabled('one')
+        expect(subject).to_not be_capability_enabled('two')
+      end
+    end
+
+    context 'when the provider is confirmed' do
+      let(:confirmed) { true }
+
+      it 'returns true for enabled and false for disabled or missing capabilities' do
+        expect(subject).to_not be_capability_enabled('one')
+        expect(subject).to be_capability_enabled('two')
+        expect(subject).to_not be_capability_enabled('three')
+      end
+    end
+  end
+
+  describe '#server_private_key' do
+    subject { Fabricate(:fasp_provider) }
+
+    it 'returns an OpenSSL::PKey::PKey' do
+      expect(subject.server_private_key).to be_a OpenSSL::PKey::PKey
+    end
+  end
+
+  describe '#server_public_key_base64' do
+    subject { Fabricate(:fasp_provider) }
+
+    it 'returns the server public key base64 encoded' do
+      expect(subject.server_public_key_base64).to eq 'T2RHkakkqAOWEMRYv9OY7LGsuIcAdmBlxuXOKax6sjw='
+    end
+  end
+
+  describe '#provider_public_key_base64=' do
+    subject { Fabricate(:fasp_provider) }
+
+    it 'allows setting the provider public key from a base64 encoded raw key' do
+      subject.provider_public_key_base64 = '9qgjOfWRhozWc9dwx5JmbshizZ7TyPBhYk9+b5tE3e4='
+
+      expect(subject.provider_public_key_pem).to eq "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA9qgjOfWRhozWc9dwx5JmbshizZ7TyPBhYk9+b5tE3e4=\n-----END PUBLIC KEY-----\n"
+    end
+  end
+
+  describe '#provider_public_key' do
+    subject { Fabricate(:fasp_provider) }
+
+    it 'returns an OpenSSL::PKey::PKey' do
+      expect(subject.provider_public_key).to be_a OpenSSL::PKey::PKey
+    end
+  end
+
+  describe '#provider_public_key_raw' do
+    subject { Fabricate(:fasp_provider) }
+
+    it 'returns a string comprised of raw bytes' do
+      expect(subject.provider_public_key_raw).to be_a String
+      expect(subject.provider_public_key_raw.encoding).to eq Encoding::BINARY
+    end
+  end
+
+  describe '#provider_public_key_fingerprint' do
+    subject { Fabricate(:fasp_provider) }
+
+    it 'returns a base64 encoded sha256 hash of the public key' do
+      expect(subject.provider_public_key_fingerprint).to eq '/AmW9EMlVq4o+Qcu9lNfTE8Ss/v9+evMPtyj2R437qE='
+    end
+  end
+
+  describe '#url' do
+    subject { Fabricate(:fasp_provider, base_url: 'https://myprovider.example.com/fasp_base/') }
+
+    it 'returns a full URL for a given path' do
+      url = subject.url('/test_path')
+      expect(url).to eq 'https://myprovider.example.com/fasp_base/test_path'
+    end
+  end
+
+  describe '#update_info!' do
+    subject { Fabricate(:fasp_provider, base_url: 'https://myprov.example.com/fasp/') }
+
+    before do
+      stub_provider_request(subject,
+                            path: '/provider_info',
+                            response_body: {
+                              capabilities: [
+                                { id: 'debug', version: '0.1' },
+                              ],
+                              contactEmail: 'newcontact@example.com',
+                              fediverseAccount: '@newfedi@social.example.com',
+                              privacyPolicy: 'https::///example.com/privacy',
+                              signInUrl: 'https://myprov.example.com/sign_in',
+                            })
+    end
+
+    context 'when setting confirm to `true`' do
+      it 'updates the provider and marks it as `confirmed`' do
+        subject.update_info!(confirm: true)
+
+        expect(subject.contact_email).to eq 'newcontact@example.com'
+        expect(subject.fediverse_account).to eq '@newfedi@social.example.com'
+        expect(subject.privacy_policy).to eq 'https::///example.com/privacy'
+        expect(subject.sign_in_url).to eq 'https://myprov.example.com/sign_in'
+        expect(subject).to be_confirmed
+        expect(subject).to be_persisted
+      end
+    end
+
+    context 'when setting confirm to `false`' do
+      it 'updates the provider but does not mark it as `confirmed`' do
+        subject.update_info!
+
+        expect(subject.contact_email).to eq 'newcontact@example.com'
+        expect(subject.fediverse_account).to eq '@newfedi@social.example.com'
+        expect(subject.privacy_policy).to eq 'https::///example.com/privacy'
+        expect(subject.sign_in_url).to eq 'https://myprov.example.com/sign_in'
+        expect(subject).to_not be_confirmed
+        expect(subject).to be_persisted
+      end
+    end
+  end
+end
diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb
deleted file mode 100644
index 587e0a9d26..0000000000
--- a/spec/models/import_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Import do
-  describe 'Validations' do
-    it { is_expected.to validate_presence_of(:type) }
-    it { is_expected.to validate_presence_of(:data) }
-  end
-end
diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index 5f91ae0967..43e9ed087b 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -295,12 +295,21 @@ RSpec.describe MediaAttachment, :attachment_processing do
     end
 
     it 'queues CacheBusterWorker jobs' do
-      original_path = media.file.path(:original)
-      small_path = media.file.path(:small)
+      original_url = media.file.url(:original)
+      small_url = media.file.url(:small)
 
       expect { media.destroy }
-        .to enqueue_sidekiq_job(CacheBusterWorker).with(original_path)
-        .and enqueue_sidekiq_job(CacheBusterWorker).with(small_path)
+        .to enqueue_sidekiq_job(CacheBusterWorker).with(original_url)
+        .and enqueue_sidekiq_job(CacheBusterWorker).with(small_url)
+    end
+
+    context 'with a missing remote attachment' do
+      let(:media) { Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png', file: nil) }
+
+      it 'does not queue CacheBusterWorker jobs' do
+        expect { media.destroy }
+          .to_not enqueue_sidekiq_job(CacheBusterWorker)
+      end
     end
   end
 
diff --git a/spec/policies/admin/fasp/provider_policy_spec.rb b/spec/policies/admin/fasp/provider_policy_spec.rb
new file mode 100644
index 0000000000..802760f2e9
--- /dev/null
+++ b/spec/policies/admin/fasp/provider_policy_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Admin::Fasp::ProviderPolicy, type: :policy do
+  subject { described_class }
+
+  let(:admin) { Fabricate(:admin_user).account }
+  let(:user) { Fabricate(:account) }
+
+  shared_examples 'admin only' do |target|
+    let(:provider) { target.is_a?(Symbol) ? Fabricate(target) : target }
+
+    context 'with an admin' do
+      it 'permits' do
+        expect(subject).to permit(admin, provider)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(subject).to_not permit(user, provider)
+      end
+    end
+  end
+
+  permissions :index?, :create? do
+    include_examples 'admin only', Fasp::Provider
+  end
+
+  permissions :show?, :create?, :update?, :destroy? do
+    include_examples 'admin only', :fasp_provider
+  end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 6564f17372..7d63ea6300 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -2,7 +2,7 @@
 
 ENV['RAILS_ENV'] ||= 'test'
 
-unless ENV['DISABLE_SIMPLECOV'] == 'true'
+if ENV.fetch('COVERAGE', false)
   require 'simplecov'
 
   SimpleCov.start 'rails' do
diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/requests/activitypub/collections_spec.rb
similarity index 59%
rename from spec/controllers/activitypub/collections_controller_spec.rb
rename to spec/requests/activitypub/collections_spec.rb
index 408e0dd2f6..d2761f98ea 100644
--- a/spec/controllers/activitypub/collections_controller_spec.rb
+++ b/spec/requests/activitypub/collections_spec.rb
@@ -2,22 +2,19 @@
 
 require 'rails_helper'
 
-RSpec.describe ActivityPub::CollectionsController do
+RSpec.describe 'ActivityPub Collections' do
   let!(:account) { Fabricate(:account) }
   let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) }
   let(:remote_account) { nil }
 
   before do
-    allow(controller).to receive(:signed_request_actor).and_return(remote_account)
-
-    Fabricate(:status_pin, account: account)
-    Fabricate(:status_pin, account: account)
+    Fabricate.times(2, :status_pin, account: account)
     Fabricate(:status_pin, account: account, status: private_pinned)
     Fabricate(:status, account: account, visibility: :private)
   end
 
   describe 'GET #show' do
-    subject(:response) { get :show, params: { id: id, account_username: account.username } }
+    subject { get account_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account }
 
     context 'when id is "featured"' do
       let(:id) { 'featured' }
@@ -26,10 +23,13 @@ RSpec.describe ActivityPub::CollectionsController do
         let(:remote_account) { nil }
 
         it 'returns http success and correct media type and correct items' do
+          subject
+
           expect(response)
             .to have_http_status(200)
             .and have_cacheable_headers
-          expect(response.media_type).to eq 'application/activity+json'
+          expect(response.media_type)
+            .to eq 'application/activity+json'
 
           expect(response.parsed_body[:orderedItems])
             .to be_an(Array)
@@ -45,17 +45,21 @@ RSpec.describe ActivityPub::CollectionsController do
           end
 
           it 'returns http gone' do
-            expect(response).to have_http_status(410)
+            subject
+
+            expect(response)
+              .to have_http_status(410)
           end
         end
 
         context 'when account is temporarily suspended' do
-          before do
-            account.suspend!
-          end
+          before { account.suspend! }
 
           it 'returns http forbidden' do
-            expect(response).to have_http_status(403)
+            subject
+
+            expect(response)
+              .to have_http_status(403)
           end
         end
       end
@@ -65,11 +69,14 @@ RSpec.describe ActivityPub::CollectionsController do
 
         context 'when getting a featured resource' do
           it 'returns http success and correct media type and expected items' do
+            subject
+
             expect(response)
               .to have_http_status(200)
               .and have_cacheable_headers
 
-            expect(response.media_type).to eq 'application/activity+json'
+            expect(response.media_type)
+              .to eq 'application/activity+json'
 
             expect(response.parsed_body[:orderedItems])
               .to be_an(Array)
@@ -80,39 +87,45 @@ RSpec.describe ActivityPub::CollectionsController do
         end
 
         context 'with authorized fetch mode' do
-          before do
-            allow(controller).to receive(:authorized_fetch_mode?).and_return(true)
-          end
+          before { Setting.authorized_fetch = true }
 
           context 'when signed request account is blocked' do
-            before do
-              account.block!(remote_account)
-            end
+            before { account.block!(remote_account) }
 
             it 'returns http success and correct media type and cache headers and empty items' do
-              expect(response).to have_http_status(200)
-              expect(response.media_type).to eq 'application/activity+json'
-              expect(response.headers['Cache-Control']).to include 'private'
+              subject
 
-              expect(response.parsed_body[:orderedItems])
-                .to be_an(Array)
-                .and be_empty
+              expect(response)
+                .to have_http_status(200)
+              expect(response.media_type)
+                .to eq('application/activity+json')
+              expect(response.headers['Cache-Control'])
+                .to include('private')
+
+              expect(response.parsed_body)
+                .to include(
+                  orderedItems: be_an(Array).and(be_empty)
+                )
             end
           end
 
           context 'when signed request account is domain blocked' do
-            before do
-              account.block_domain!(remote_account.domain)
-            end
+            before { account.block_domain!(remote_account.domain) }
 
             it 'returns http success and correct media type and cache headers and empty items' do
-              expect(response).to have_http_status(200)
-              expect(response.media_type).to eq 'application/activity+json'
-              expect(response.headers['Cache-Control']).to include 'private'
+              subject
 
-              expect(response.parsed_body[:orderedItems])
-                .to be_an(Array)
-                .and be_empty
+              expect(response)
+                .to have_http_status(200)
+              expect(response.media_type)
+                .to eq('application/activity+json')
+              expect(response.headers['Cache-Control'])
+                .to include('private')
+
+              expect(response.parsed_body)
+                .to include(
+                  orderedItems: be_an(Array).and(be_empty)
+                )
             end
           end
         end
@@ -123,7 +136,10 @@ RSpec.describe ActivityPub::CollectionsController do
       let(:id) { 'hoge' }
 
       it 'returns http not found' do
-        expect(response).to have_http_status(404)
+        subject
+
+        expect(response)
+          .to have_http_status(404)
       end
     end
   end
diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/requests/activitypub/followers_synchronizations_spec.rb
similarity index 68%
rename from spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
rename to spec/requests/activitypub/followers_synchronizations_spec.rb
index cbd982f18f..97b8a7908e 100644
--- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
+++ b/spec/requests/activitypub/followers_synchronizations_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-RSpec.describe ActivityPub::FollowersSynchronizationsController do
+RSpec.describe 'ActivityPub Follower Synchronizations' do
   let!(:account) { Fabricate(:account) }
   let!(:follower_example_com_user_a) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') }
   let!(:follower_example_com_user_b) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') }
@@ -14,32 +14,34 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do
     follower_example_com_user_b.follow!(account)
     follower_foo_com_user_a.follow!(account)
     follower_example_com_instance_actor.follow!(account)
-
-    allow(controller).to receive(:signed_request_actor).and_return(remote_account)
   end
 
   describe 'GET #show' do
     context 'without signature' do
-      let(:remote_account) { nil }
-
-      before do
-        get :show, params: { account_username: account.username }
-      end
+      subject { get account_followers_synchronization_path(account_username: account.username) }
 
       it 'returns http not authorized' do
-        expect(response).to have_http_status(401)
+        subject
+
+        expect(response)
+          .to have_http_status(401)
       end
     end
 
     context 'with signature from example.com' do
-      subject(:response) { get :show, params: { account_username: account.username } }
+      subject { get account_followers_synchronization_path(account_username: account.username), headers: nil, sign_with: remote_account }
 
       let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') }
 
       it 'returns http success and cache control and activity json types and correct items' do
-        expect(response).to have_http_status(200)
-        expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
-        expect(response.media_type).to eq 'application/activity+json'
+        subject
+
+        expect(response)
+          .to have_http_status(200)
+        expect(response.headers['Cache-Control'])
+          .to eq 'max-age=0, private'
+        expect(response.media_type)
+          .to eq 'application/activity+json'
 
         expect(response.parsed_body[:orderedItems])
           .to be_an(Array)
@@ -57,17 +59,21 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do
         end
 
         it 'returns http gone' do
-          expect(response).to have_http_status(410)
+          subject
+
+          expect(response)
+            .to have_http_status(410)
         end
       end
 
       context 'when account is temporarily suspended' do
-        before do
-          account.suspend!
-        end
+        before { account.suspend! }
 
         it 'returns http forbidden' do
-          expect(response).to have_http_status(403)
+          subject
+
+          expect(response)
+            .to have_http_status(403)
         end
       end
     end
diff --git a/spec/requests/activitypub/inboxes_spec.rb b/spec/requests/activitypub/inboxes_spec.rb
new file mode 100644
index 0000000000..b21881b10f
--- /dev/null
+++ b/spec/requests/activitypub/inboxes_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'ActivityPub Inboxes' do
+  let(:remote_account) { nil }
+
+  describe 'POST #create' do
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) }
+
+      context 'without a named account' do
+        subject { post inbox_path, params: {}.to_json, sign_with: remote_account }
+
+        it 'returns http accepted' do
+          subject
+
+          expect(response)
+            .to have_http_status(202)
+        end
+      end
+
+      context 'with a specific account' do
+        subject { post account_inbox_path(account_username: account.username), params: {}.to_json, sign_with: remote_account }
+
+        let(:account) { Fabricate(:account) }
+
+        context 'when account is permanently suspended' do
+          before do
+            account.suspend!
+            account.deletion_request.destroy
+          end
+
+          it 'returns http gone' do
+            subject
+
+            expect(response)
+              .to have_http_status(410)
+          end
+        end
+
+        context 'when account is temporarily suspended' do
+          before { account.suspend! }
+
+          it 'returns http accepted' do
+            subject
+
+            expect(response)
+              .to have_http_status(202)
+          end
+        end
+      end
+    end
+
+    context 'with Collection-Synchronization header' do
+      subject { post inbox_path, params: {}.to_json, headers: { 'Collection-Synchronization' => synchronization_header }, sign_with: remote_account }
+
+      let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) }
+      let(:synchronization_collection) { remote_account.followers_url }
+      let(:synchronization_url) { 'https://example.com/followers-for-domain' }
+      let(:synchronization_hash) { 'somehash' }
+      let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" }
+
+      before do
+        stub_follow_sync_worker
+        stub_followers_hash
+      end
+
+      context 'with mismatching target collection' do
+        let(:synchronization_collection) { 'https://example.com/followers2' }
+
+        it 'does not start a synchronization job' do
+          subject
+
+          expect(response)
+            .to have_http_status(202)
+          expect(ActivityPub::FollowersSynchronizationWorker)
+            .to_not have_received(:perform_async)
+        end
+      end
+
+      context 'with mismatching domain in partial collection attribute' do
+        let(:synchronization_url) { 'https://example.org/followers' }
+
+        it 'does not start a synchronization job' do
+          subject
+
+          expect(response)
+            .to have_http_status(202)
+          expect(ActivityPub::FollowersSynchronizationWorker)
+            .to_not have_received(:perform_async)
+        end
+      end
+
+      context 'with matching digest' do
+        it 'does not start a synchronization job' do
+          subject
+
+          expect(response)
+            .to have_http_status(202)
+          expect(ActivityPub::FollowersSynchronizationWorker)
+            .to_not have_received(:perform_async)
+        end
+      end
+
+      context 'with mismatching digest' do
+        let(:synchronization_hash) { 'wronghash' }
+
+        it 'starts a synchronization job' do
+          subject
+
+          expect(response)
+            .to have_http_status(202)
+          expect(ActivityPub::FollowersSynchronizationWorker)
+            .to have_received(:perform_async)
+        end
+      end
+
+      it 'returns http accepted' do
+        subject
+
+        expect(response)
+          .to have_http_status(202)
+      end
+
+      def stub_follow_sync_worker
+        allow(ActivityPub::FollowersSynchronizationWorker)
+          .to receive(:perform_async)
+          .and_return(nil)
+      end
+
+      def stub_followers_hash
+        Rails.cache.write("followers_hash:#{remote_account.id}:local", 'somehash') # Populate value to match request
+      end
+    end
+
+    context 'without signature' do
+      subject { post inbox_path, params: {}.to_json }
+
+      it 'returns http not authorized' do
+        subject
+
+        expect(response)
+          .to have_http_status(401)
+      end
+    end
+  end
+end
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/requests/activitypub/outboxes_spec.rb
similarity index 63%
rename from spec/controllers/activitypub/outboxes_controller_spec.rb
rename to spec/requests/activitypub/outboxes_spec.rb
index ca986dcabb..22b2f97c07 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/requests/activitypub/outboxes_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-RSpec.describe ActivityPub::OutboxesController do
+RSpec.describe 'ActivityPub Outboxes' do
   let!(:account) { Fabricate(:account) }
 
   before do
@@ -11,13 +11,11 @@ RSpec.describe ActivityPub::OutboxesController do
     Fabricate(:status, account: account, visibility: :private)
     Fabricate(:status, account: account, visibility: :direct)
     Fabricate(:status, account: account, visibility: :limited)
-
-    allow(controller).to receive(:signed_request_actor).and_return(remote_account)
   end
 
   describe 'GET #show' do
     context 'without signature' do
-      subject(:response) { get :show, params: { account_username: account.username, page: page } }
+      subject { get account_outbox_path(account_username: account.username, page: page) }
 
       let(:remote_account) { nil }
 
@@ -25,13 +23,18 @@ RSpec.describe ActivityPub::OutboxesController do
         let(:page) { nil }
 
         it 'returns http success and correct media type and headers and items count' do
+          subject
+
           expect(response)
             .to have_http_status(200)
             .and have_cacheable_headers
 
-          expect(response.media_type).to eq 'application/activity+json'
-          expect(response.headers['Vary']).to be_nil
-          expect(response.parsed_body[:totalItems]).to eq 4
+          expect(response.media_type)
+            .to eq 'application/activity+json'
+          expect(response.headers['Vary'])
+            .to be_nil
+          expect(response.parsed_body[:totalItems])
+            .to eq 4
         end
 
         context 'when account is permanently suspended' do
@@ -41,17 +44,21 @@ RSpec.describe ActivityPub::OutboxesController do
           end
 
           it 'returns http gone' do
-            expect(response).to have_http_status(410)
+            subject
+
+            expect(response)
+              .to have_http_status(410)
           end
         end
 
         context 'when account is temporarily suspended' do
-          before do
-            account.suspend!
-          end
+          before { account.suspend! }
 
           it 'returns http forbidden' do
-            expect(response).to have_http_status(403)
+            subject
+
+            expect(response)
+              .to have_http_status(403)
           end
         end
       end
@@ -60,12 +67,16 @@ RSpec.describe ActivityPub::OutboxesController do
         let(:page) { 'true' }
 
         it 'returns http success and correct media type and vary header and items' do
+          subject
+
           expect(response)
             .to have_http_status(200)
             .and have_cacheable_headers
 
-          expect(response.media_type).to eq 'application/activity+json'
-          expect(response.headers['Vary']).to include 'Signature'
+          expect(response.media_type)
+            .to eq 'application/activity+json'
+          expect(response.headers['Vary'])
+            .to include 'Signature'
 
           expect(response.parsed_body)
             .to include(
@@ -82,35 +93,42 @@ RSpec.describe ActivityPub::OutboxesController do
           end
 
           it 'returns http gone' do
-            expect(response).to have_http_status(410)
+            subject
+
+            expect(response)
+              .to have_http_status(410)
           end
         end
 
         context 'when account is temporarily suspended' do
-          before do
-            account.suspend!
-          end
+          before { account.suspend! }
 
           it 'returns http forbidden' do
-            expect(response).to have_http_status(403)
+            subject
+
+            expect(response)
+              .to have_http_status(403)
           end
         end
       end
     end
 
     context 'with signature' do
+      subject { get account_outbox_path(account_username: account.username, page: page), headers: nil, sign_with: remote_account }
+
       let(:remote_account) { Fabricate(:account, domain: 'example.com') }
       let(:page) { 'true' }
 
       context 'when signed request account does not follow account' do
-        before do
-          get :show, params: { account_username: account.username, page: page }
-        end
-
         it 'returns http success and correct media type and headers and items' do
-          expect(response).to have_http_status(200)
-          expect(response.media_type).to eq 'application/activity+json'
-          expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
+          subject
+
+          expect(response)
+            .to have_http_status(200)
+          expect(response.media_type)
+            .to eq 'application/activity+json'
+          expect(response.headers['Cache-Control'])
+            .to eq 'private, no-store'
 
           expect(response.parsed_body)
             .to include(
@@ -122,15 +140,17 @@ RSpec.describe ActivityPub::OutboxesController do
       end
 
       context 'when signed request account follows account' do
-        before do
-          remote_account.follow!(account)
-          get :show, params: { account_username: account.username, page: page }
-        end
+        before { remote_account.follow!(account) }
 
         it 'returns http success and correct media type and headers and items' do
-          expect(response).to have_http_status(200)
-          expect(response.media_type).to eq 'application/activity+json'
-          expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
+          subject
+
+          expect(response)
+            .to have_http_status(200)
+          expect(response.media_type)
+            .to eq 'application/activity+json'
+          expect(response.headers['Cache-Control'])
+            .to eq 'private, no-store'
 
           expect(response.parsed_body)
             .to include(
@@ -142,15 +162,17 @@ RSpec.describe ActivityPub::OutboxesController do
       end
 
       context 'when signed request account is blocked' do
-        before do
-          account.block!(remote_account)
-          get :show, params: { account_username: account.username, page: page }
-        end
+        before { account.block!(remote_account) }
 
         it 'returns http success and correct media type and headers and items' do
-          expect(response).to have_http_status(200)
-          expect(response.media_type).to eq 'application/activity+json'
-          expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
+          subject
+
+          expect(response)
+            .to have_http_status(200)
+          expect(response.media_type)
+            .to eq 'application/activity+json'
+          expect(response.headers['Cache-Control'])
+            .to eq 'private, no-store'
 
           expect(response.parsed_body)
             .to include(
@@ -160,15 +182,17 @@ RSpec.describe ActivityPub::OutboxesController do
       end
 
       context 'when signed request account is domain blocked' do
-        before do
-          account.block_domain!(remote_account.domain)
-          get :show, params: { account_username: account.username, page: page }
-        end
+        before { account.block_domain!(remote_account.domain) }
 
         it 'returns http success and correct media type and headers and items' do
-          expect(response).to have_http_status(200)
-          expect(response.media_type).to eq 'application/activity+json'
-          expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
+          subject
+
+          expect(response)
+            .to have_http_status(200)
+          expect(response.media_type)
+            .to eq 'application/activity+json'
+          expect(response.headers['Cache-Control'])
+            .to eq 'private, no-store'
 
           expect(response.parsed_body)
             .to include(
diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/requests/activitypub/replies_spec.rb
similarity index 78%
rename from spec/controllers/activitypub/replies_controller_spec.rb
rename to spec/requests/activitypub/replies_spec.rb
index d7c2c2d3b0..313cab2a44 100644
--- a/spec/controllers/activitypub/replies_controller_spec.rb
+++ b/spec/requests/activitypub/replies_spec.rb
@@ -2,9 +2,9 @@
 
 require 'rails_helper'
 
-RSpec.describe ActivityPub::RepliesController do
+RSpec.describe 'ActivityPub Replies' do
   let(:status) { Fabricate(:status, visibility: parent_visibility) }
-  let(:remote_account)  { Fabricate(:account, domain: 'foobar.com') }
+  let(:remote_account) { Fabricate(:account, domain: 'foobar.com') }
   let(:remote_reply_id) { 'https://foobar.com/statuses/1234' }
   let(:remote_querier) { nil }
 
@@ -13,7 +13,10 @@ RSpec.describe ActivityPub::RepliesController do
       let(:parent_visibility) { :private }
 
       it 'returns http not found' do
-        expect(response).to have_http_status(404)
+        subject
+
+        expect(response)
+          .to have_http_status(404)
       end
     end
 
@@ -21,7 +24,10 @@ RSpec.describe ActivityPub::RepliesController do
       let(:parent_visibility) { :direct }
 
       it 'returns http not found' do
-        expect(response).to have_http_status(404)
+        subject
+
+        expect(response)
+          .to have_http_status(404)
       end
     end
   end
@@ -31,7 +37,10 @@ RSpec.describe ActivityPub::RepliesController do
       let(:parent_visibility) { :public }
 
       it 'returns http not found' do
-        expect(response).to have_http_status(404)
+        subject
+
+        expect(response)
+          .to have_http_status(404)
       end
     end
 
@@ -48,19 +57,23 @@ RSpec.describe ActivityPub::RepliesController do
       end
 
       it 'returns http gone' do
-        expect(response).to have_http_status(410)
+        subject
+
+        expect(response)
+          .to have_http_status(410)
       end
     end
 
     context 'when account is temporarily suspended' do
       let(:parent_visibility) { :public }
 
-      before do
-        status.account.suspend!
-      end
+      before { status.account.suspend! }
 
       it 'returns http forbidden' do
-        expect(response).to have_http_status(403)
+        subject
+
+        expect(response)
+          .to have_http_status(403)
       end
     end
 
@@ -68,15 +81,20 @@ RSpec.describe ActivityPub::RepliesController do
       let(:parent_visibility) { :public }
 
       it 'returns http success and correct media type' do
+        subject
+
         expect(response)
           .to have_http_status(200)
           .and have_cacheable_headers
 
-        expect(response.media_type).to eq 'application/activity+json'
+        expect(response.media_type)
+          .to eq 'application/activity+json'
       end
 
-      context 'without only_other_accounts' do
+      context 'without `only_other_accounts` param' do
         it "returns items with thread author's replies" do
+          subject
+
           expect(response.parsed_body)
             .to include(
               first: be_a(Hash).and(
@@ -91,6 +109,8 @@ RSpec.describe ActivityPub::RepliesController do
 
         context 'when there are few self-replies' do
           it 'points next to replies from other people' do
+            subject
+
             expect(response.parsed_body)
               .to include(
                 first: be_a(Hash).and(
@@ -108,6 +128,8 @@ RSpec.describe ActivityPub::RepliesController do
           end
 
           it 'points next to other self-replies' do
+            subject
+
             expect(response.parsed_body)
               .to include(
                 first: be_a(Hash).and(
@@ -120,31 +142,33 @@ RSpec.describe ActivityPub::RepliesController do
         end
       end
 
-      context 'with only_other_accounts' do
+      context 'with `only_other_accounts` param' do
         let(:only_other_accounts) { 'true' }
 
-        it 'returns items with other public or unlisted replies' do
+        it 'returns items with other public or unlisted replies and correctly inlines replies and uses IDs' do
+          subject
+
           expect(response.parsed_body)
             .to include(
               first: be_a(Hash).and(
                 include(items: be_an(Array).and(have_attributes(size: 3)))
               )
             )
-        end
 
-        it 'only inlines items that are local and public or unlisted replies' do
+          # Only inline replies that are local and public, or unlisted
           expect(inlined_replies)
             .to all(satisfy { |item| targets_public_collection?(item) })
             .and all(satisfy { |item| ActivityPub::TagManager.instance.local_uri?(item[:id]) })
-        end
 
-        it 'uses ids for remote toots' do
+          # Use ids for remote replies
           expect(remote_replies)
             .to all(satisfy { |item| item.is_a?(String) && !ActivityPub::TagManager.instance.local_uri?(item) })
         end
 
         context 'when there are few replies' do
           it 'does not have a next page' do
+            subject
+
             expect(response.parsed_body)
               .to include(
                 first: be_a(Hash).and(not_include(next: be_present))
@@ -158,6 +182,8 @@ RSpec.describe ActivityPub::RepliesController do
           end
 
           it 'points next to other replies' do
+            subject
+
             expect(response.parsed_body)
               .to include(
                 first: be_a(Hash).and(
@@ -176,10 +202,8 @@ RSpec.describe ActivityPub::RepliesController do
 
   before do
     stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5
-    allow(controller).to receive(:signed_request_actor).and_return(remote_querier)
 
-    Fabricate(:status, thread: status, visibility: :public)
-    Fabricate(:status, thread: status, visibility: :public)
+    Fabricate.times(2, :status, thread: status, visibility: :public)
     Fabricate(:status, thread: status, visibility: :private)
     Fabricate(:status, account: status.account, thread: status, visibility: :public)
     Fabricate(:status, account: status.account, thread: status, visibility: :private)
@@ -188,31 +212,29 @@ RSpec.describe ActivityPub::RepliesController do
   end
 
   describe 'GET #index' do
-    subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts } }
-
     let(:only_other_accounts) { nil }
 
     context 'with no signature' do
+      subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts) }
+
       it_behaves_like 'allowed access'
     end
 
     context 'with signature' do
+      subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts), headers: nil, sign_with: remote_querier }
+
       let(:remote_querier) { Fabricate(:account, domain: 'example.com') }
 
       it_behaves_like 'allowed access'
 
       context 'when signed request account is blocked' do
-        before do
-          status.account.block!(remote_querier)
-        end
+        before { status.account.block!(remote_querier) }
 
         it_behaves_like 'disallowed access'
       end
 
       context 'when signed request account is domain blocked' do
-        before do
-          status.account.block_domain!(remote_querier.domain)
-        end
+        before { status.account.block_domain!(remote_querier.domain) }
 
         it_behaves_like 'disallowed access'
       end
diff --git a/spec/requests/admin/instances_spec.rb b/spec/requests/admin/instances_spec.rb
new file mode 100644
index 0000000000..afb6602a22
--- /dev/null
+++ b/spec/requests/admin/instances_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Instances' do
+  describe 'GET /admin/instances/:id' do
+    context 'with an unknown domain' do
+      before { sign_in Fabricate(:admin_user) }
+
+      it 'returns http success' do
+        get admin_instance_path(id: 'unknown.example')
+
+        expect(response)
+          .to have_http_status(200)
+      end
+    end
+  end
+end
diff --git a/spec/requests/admin/roles_spec.rb b/spec/requests/admin/roles_spec.rb
index 785da5a0ff..21853eb203 100644
--- a/spec/requests/admin/roles_spec.rb
+++ b/spec/requests/admin/roles_spec.rb
@@ -3,14 +3,142 @@
 require 'rails_helper'
 
 RSpec.describe 'Admin Roles' do
-  describe 'POST /admin/roles' do
+  context 'when signed in as lower permissions user' do
+    let(:user_role) { Fabricate(:user_role, permissions: UserRole::Flags::NONE) }
+
+    before { sign_in Fabricate(:user, role: user_role) }
+
+    describe 'GET /admin/roles' do
+      it 'returns http forbidden' do
+        get admin_roles_path
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+
+    describe 'GET /admin/roles/new' do
+      it 'returns http forbidden' do
+        get new_admin_role_path
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+
+    describe 'GET /admin/roles/:id/edit' do
+      let(:role) { Fabricate(:user_role) }
+
+      it 'returns http forbidden' do
+        get edit_admin_role_path(role)
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+
+    describe 'PUT /admin/roles/:id' do
+      let(:role) { Fabricate(:user_role) }
+
+      it 'returns http forbidden' do
+        put admin_role_path(role)
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+
+    describe 'DELETE /admin/roles/:id' do
+      let(:role) { Fabricate(:user_role) }
+
+      it 'returns http forbidden' do
+        delete admin_role_path(role)
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+  end
+
+  context 'when user has permissions to manage roles' do
+    let(:user_role) { Fabricate(:user_role, permissions: UserRole::FLAGS[:manage_users]) }
+
+    before { sign_in Fabricate(:user, role: user_role) }
+
+    context 'when target role permission outranks user' do
+      let(:role) { Fabricate(:user_role, position: user_role.position + 1) }
+
+      describe 'GET /admin/roles/:id/edit' do
+        it 'returns http forbidden' do
+          get edit_admin_role_path(role)
+
+          expect(response)
+            .to have_http_status(403)
+        end
+      end
+
+      describe 'PUT /admin/roles/:id' do
+        it 'returns http forbidden' do
+          put admin_role_path(role)
+
+          expect(response)
+            .to have_http_status(403)
+        end
+      end
+
+      describe 'DELETE /admin/roles/:id' do
+        it 'returns http forbidden' do
+          delete admin_role_path(role)
+
+          expect(response)
+            .to have_http_status(403)
+        end
+      end
+    end
+  end
+
+  context 'when attempting to add permissions the user does not have' do
+    let(:user_role) { Fabricate(:user_role, permissions: UserRole::FLAGS[:manage_roles], position: 5) }
+
+    before { sign_in Fabricate(:user, role: user_role) }
+
+    describe 'POST /admin/roles' do
+      subject { post admin_roles_path, params: { user_role: { name: 'Bar', position: 2, permissions_as_keys: %w(manage_roles manage_users manage_reports) } } }
+
+      it 'does not create role' do
+        expect { subject }
+          .to_not change(UserRole, :count)
+
+        expect(response.body)
+          .to include(I18n.t('admin.roles.add_new'))
+      end
+    end
+
+    describe 'PUT /admin/roles/:id' do
+      subject { put admin_role_path(role), params: { user_role: { position: 2, permissions_as_keys: %w(manage_roles manage_users manage_reports) } } }
+
+      let(:role) { Fabricate(:user_role, name: 'Bar') }
+
+      it 'does not create role' do
+        expect { subject }
+          .to_not(change { role.reload.permissions })
+
+        expect(response.parsed_body.title)
+          .to match(I18n.t('admin.roles.edit', name: 'Bar'))
+      end
+    end
+  end
+
+  context 'when signed in as admin' do
     before { sign_in Fabricate(:admin_user) }
 
-    it 'gracefully handles invalid nested params' do
-      post admin_roles_path(user_role: 'invalid')
+    describe 'POST /admin/roles' do
+      it 'gracefully handles invalid nested params' do
+        post admin_roles_path(user_role: 'invalid')
 
-      expect(response)
-        .to have_http_status(400)
+        expect(response)
+          .to have_http_status(400)
+      end
     end
   end
 end
diff --git a/spec/requests/admin/settings/branding_spec.rb b/spec/requests/admin/settings/branding_spec.rb
new file mode 100644
index 0000000000..e5206f056f
--- /dev/null
+++ b/spec/requests/admin/settings/branding_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Settings Branding' do
+  describe 'When signed in as an admin' do
+    before { sign_in Fabricate(:admin_user) }
+
+    describe 'PUT /admin/settings/branding' do
+      it 'cannot create a setting value for a non-admin key' do
+        expect { put admin_settings_branding_path, params: { form_admin_settings: { new_setting_key: 'New key value' } } }
+          .to_not change(Setting, :new_setting_key).from(nil)
+
+        expect(response)
+          .to have_http_status(400)
+      end
+    end
+  end
+end
diff --git a/spec/requests/admin/users/roles_spec.rb b/spec/requests/admin/users/roles_spec.rb
index b39e3f8bae..fb88e4c87a 100644
--- a/spec/requests/admin/users/roles_spec.rb
+++ b/spec/requests/admin/users/roles_spec.rb
@@ -3,6 +3,34 @@
 require 'rails_helper'
 
 RSpec.describe 'Admin Users Roles' do
+  context 'when target user is higher ranked than current user' do
+    let(:current_role) { UserRole.create(name: 'Foo', permissions: UserRole::FLAGS[:manage_roles], position: 10) }
+    let(:current_user) { Fabricate(:user, role: current_role) }
+
+    let(:previous_role) { UserRole.create(name: 'Baz', permissions: UserRole::FLAGS[:administrator], position: 100) }
+    let(:user) { Fabricate(:user, role: previous_role) }
+
+    before { sign_in(current_user) }
+
+    describe 'GET /admin/users/:user_id/role' do
+      it 'returns http forbidden' do
+        get admin_user_role_path(user.id)
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+
+    describe 'PUT /admin/users/:user_id/role' do
+      it 'returns http forbidden' do
+        put admin_user_role_path(user.id)
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+  end
+
   describe 'PUT /admin/users/:user_id/role' do
     before { sign_in Fabricate(:admin_user) }
 
diff --git a/spec/requests/api/fasp/debug/v0/callback/responses_spec.rb b/spec/requests/api/fasp/debug/v0/callback/responses_spec.rb
new file mode 100644
index 0000000000..58c5e8897b
--- /dev/null
+++ b/spec/requests/api/fasp/debug/v0/callback/responses_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Api::Fasp::Debug::V0::Callback::Responses', feature: :fasp do
+  include ProviderRequestHelper
+
+  describe 'POST /api/fasp/debug/v0/callback/responses' do
+    let(:provider) { Fabricate(:debug_fasp) }
+
+    it 'create a record of the callback' do
+      payload = { test: 'call' }
+      headers = request_authentication_headers(provider,
+                                               url: api_fasp_debug_v0_callback_responses_url,
+                                               method: :post,
+                                               body: payload)
+
+      expect do
+        post api_fasp_debug_v0_callback_responses_path, headers:, params: payload, as: :json
+      end.to change(Fasp::DebugCallback, :count).by(1)
+      expect(response).to have_http_status(201)
+
+      debug_callback = Fasp::DebugCallback.last
+      expect(debug_callback.fasp_provider).to eq provider
+      expect(debug_callback.request_body).to eq '{"test":"call"}'
+    end
+  end
+end
diff --git a/spec/requests/api/fasp/registrations_spec.rb b/spec/requests/api/fasp/registrations_spec.rb
new file mode 100644
index 0000000000..53fdfeef5c
--- /dev/null
+++ b/spec/requests/api/fasp/registrations_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Api::Fasp::Registrations', feature: :fasp do
+  describe 'POST /api/fasp/registration' do
+    subject do
+      post api_fasp_registration_path, params:
+    end
+
+    context 'when given valid data' do
+      let(:params) do
+        {
+          name: 'Test Provider',
+          baseUrl: 'https://newprovider.example.com/fasp',
+          serverId: '123',
+          publicKey: '9qgjOfWRhozWc9dwx5JmbshizZ7TyPBhYk9+b5tE3e4=',
+        }
+      end
+
+      it 'creates a new provider' do
+        expect { subject }.to change(Fasp::Provider, :count).by(1)
+
+        expect(response).to have_http_status 200
+      end
+    end
+
+    context 'when given invalid data' do
+      let(:params) do
+        {
+          name: 'incomplete',
+        }
+      end
+
+      it 'does not create a provider and returns an error code' do
+        expect { subject }.to_not change(Fasp::Provider, :count)
+
+        expect(response).to have_http_status 422
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v1/accounts/credentials_spec.rb b/spec/requests/api/v1/accounts/credentials_spec.rb
index c92f4c7973..68ea259481 100644
--- a/spec/requests/api/v1/accounts/credentials_spec.rb
+++ b/spec/requests/api/v1/accounts/credentials_spec.rb
@@ -53,8 +53,6 @@ RSpec.describe 'credentials API' do
       patch '/api/v1/accounts/update_credentials', headers: headers, params: params
     end
 
-    before { allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) }
-
     let(:params) do
       {
         avatar: fixture_file_upload('avatar.gif', 'image/gif'),
@@ -113,7 +111,7 @@ RSpec.describe 'credentials API' do
       })
 
       expect(ActivityPub::UpdateDistributionWorker)
-        .to have_received(:perform_async).with(user.account_id)
+        .to have_enqueued_sidekiq_job(user.account_id)
     end
 
     def expect_account_updates
diff --git a/spec/requests/api/v1/accounts/pins_spec.rb b/spec/requests/api/v1/accounts/endorsements_spec.rb
similarity index 57%
rename from spec/requests/api/v1/accounts/pins_spec.rb
rename to spec/requests/api/v1/accounts/endorsements_spec.rb
index 8ebcb27d28..6e0996a1f1 100644
--- a/spec/requests/api/v1/accounts/pins_spec.rb
+++ b/spec/requests/api/v1/accounts/endorsements_spec.rb
@@ -13,8 +13,30 @@ RSpec.describe 'Accounts Pins API' do
     kevin.account.followers << user.account
   end
 
-  describe 'POST /api/v1/accounts/:account_id/pin' do
-    subject { post "/api/v1/accounts/#{kevin.account.id}/pin", headers: headers }
+  describe 'GET /api/v1/accounts/:account_id/endorsements' do
+    subject { get "/api/v1/accounts/#{user.account.id}/endorsements", headers: headers }
+
+    let(:scopes) { 'read:accounts' }
+
+    before do
+      user.account.endorsed_accounts << kevin.account
+    end
+
+    it 'returns the expected accounts', :aggregate_failures do
+      subject
+
+      expect(response).to have_http_status(200)
+      expect(response.content_type)
+        .to start_with('application/json')
+      expect(response.parsed_body)
+        .to contain_exactly(
+          hash_including(id: kevin.account_id.to_s)
+        )
+    end
+  end
+
+  describe 'POST /api/v1/accounts/:account_id/endorse' do
+    subject { post "/api/v1/accounts/#{kevin.account.id}/endorse", headers: headers }
 
     it 'creates account_pin', :aggregate_failures do
       expect do
@@ -26,8 +48,8 @@ RSpec.describe 'Accounts Pins API' do
     end
   end
 
-  describe 'POST /api/v1/accounts/:account_id/unpin' do
-    subject { post "/api/v1/accounts/#{kevin.account.id}/unpin", headers: headers }
+  describe 'POST /api/v1/accounts/:account_id/unendorse' do
+    subject { post "/api/v1/accounts/#{kevin.account.id}/unendorse", headers: headers }
 
     before do
       Fabricate(:account_pin, account: user.account, target_account: kevin.account)
diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb
index d423a08f12..329bb5f1e4 100644
--- a/spec/requests/api/v1/accounts_spec.rb
+++ b/spec/requests/api/v1/accounts_spec.rb
@@ -74,12 +74,45 @@ RSpec.describe '/api/v1/accounts' do
 
   describe 'POST /api/v1/accounts' do
     subject do
-      post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement }
+      post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement, date_of_birth: date_of_birth }
     end
 
     let(:client_app) { Fabricate(:application) }
     let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) }
     let(:agreement) { nil }
+    let(:date_of_birth) { nil }
+
+    context 'when age verification is enabled' do
+      before do
+        Setting.min_age = 16
+      end
+
+      let(:agreement) { 'true' }
+
+      context 'when date of birth is below age limit' do
+        let(:date_of_birth) { 13.years.ago.strftime('%d.%m.%Y') }
+
+        it 'returns http unprocessable entity' do
+          subject
+
+          expect(response).to have_http_status(422)
+          expect(response.content_type)
+            .to start_with('application/json')
+        end
+      end
+
+      context 'when date of birth is over age limit' do
+        let(:date_of_birth) { 17.years.ago.strftime('%d.%m.%Y') }
+
+        it 'creates a user', :aggregate_failures do
+          subject
+
+          expect(response).to have_http_status(200)
+          expect(response.content_type)
+            .to start_with('application/json')
+        end
+      end
+    end
 
     context 'when given truthy agreement' do
       let(:agreement) { 'true' }
diff --git a/spec/requests/api/v1/instances/domain_blocks_spec.rb b/spec/requests/api/v1/instances/domain_blocks_spec.rb
index d0707d784c..43cc71aed5 100644
--- a/spec/requests/api/v1/instances/domain_blocks_spec.rb
+++ b/spec/requests/api/v1/instances/domain_blocks_spec.rb
@@ -9,9 +9,10 @@ RSpec.describe 'Domain Blocks' do
   let(:headers) { { Authorization: "Bearer #{token.token}" } }
 
   describe 'GET /api/v1/instance/domain_blocks' do
-    before do
-      Fabricate(:domain_block)
-    end
+    let(:user) { Fabricate(:user) }
+    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) }
+
+    before { Fabricate(:domain_block) }
 
     context 'with domain blocks set to all' do
       before { Setting.show_domain_blocks = 'all' }
@@ -45,11 +46,95 @@ RSpec.describe 'Domain Blocks' do
     context 'with domain blocks set to users' do
       before { Setting.show_domain_blocks = 'users' }
 
-      it 'returns http not found' do
-        get api_v1_instance_domain_blocks_path
+      context 'without authentication token' do
+        it 'returns http not found' do
+          get api_v1_instance_domain_blocks_path
 
-        expect(response)
-          .to have_http_status(404)
+          expect(response)
+            .to have_http_status(404)
+        end
+      end
+
+      context 'with authentication token' do
+        context 'with unapproved user' do
+          before { user.update(approved: false) }
+
+          it 'returns http not found' do
+            get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token.token}" }
+
+            expect(response)
+              .to have_http_status(404)
+          end
+        end
+
+        context 'with unconfirmed user' do
+          before { user.update(confirmed_at: nil) }
+
+          it 'returns http not found' do
+            get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token.token}" }
+
+            expect(response)
+              .to have_http_status(404)
+          end
+        end
+
+        context 'with disabled user' do
+          before { user.update(disabled: true) }
+
+          it 'returns http not found' do
+            get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token.token}" }
+
+            expect(response)
+              .to have_http_status(404)
+          end
+        end
+
+        context 'with suspended user' do
+          before { user.account.update(suspended_at: Time.zone.now) }
+
+          it 'returns http not found' do
+            get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token.token}" }
+
+            expect(response)
+              .to have_http_status(403)
+          end
+        end
+
+        context 'with moved user' do
+          before { user.account.update(moved_to_account_id: Fabricate(:account).id) }
+
+          it 'returns http success' do
+            get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token.token}" }
+
+            expect(response)
+              .to have_http_status(200)
+
+            expect(response.content_type)
+              .to start_with('application/json')
+
+            expect(response.parsed_body)
+              .to be_present
+              .and(be_an(Array))
+              .and(have_attributes(size: 1))
+          end
+        end
+
+        context 'with normal user' do
+          it 'returns http success' do
+            get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token.token}" }
+
+            expect(response)
+              .to have_http_status(200)
+
+            expect(response.content_type)
+              .to start_with('application/json')
+
+            expect(response.parsed_body)
+              .to be_present
+              .and(be_an(Array))
+              .and(have_attributes(size: 1))
+          end
+        end
       end
     end
 
diff --git a/spec/requests/api/v1/lists_spec.rb b/spec/requests/api/v1/lists_spec.rb
index 51cd797408..13a954b9ac 100644
--- a/spec/requests/api/v1/lists_spec.rb
+++ b/spec/requests/api/v1/lists_spec.rb
@@ -140,9 +140,12 @@ RSpec.describe 'Lists' do
       it 'returns http unprocessable entity' do
         subject
 
-        expect(response).to have_http_status(422)
+        expect(response)
+          .to have_http_status(422)
         expect(response.content_type)
           .to start_with('application/json')
+        expect(response.parsed_body)
+          .to include(error: /Replies policy is not included/)
       end
     end
   end
diff --git a/spec/requests/api/v1/media_spec.rb b/spec/requests/api/v1/media_spec.rb
index d7d0b92f11..4d6e250207 100644
--- a/spec/requests/api/v1/media_spec.rb
+++ b/spec/requests/api/v1/media_spec.rb
@@ -193,4 +193,57 @@ RSpec.describe 'Media' do
       end
     end
   end
+
+  describe 'DELETE /api/v1/media/:id' do
+    subject do
+      delete "/api/v1/media/#{media.id}", headers: headers
+    end
+
+    context 'when media is not attached to a status' do
+      let(:media) { Fabricate(:media_attachment, account: user.account, status: nil) }
+
+      it 'returns http empty response' do
+        subject
+
+        expect(response).to have_http_status(200)
+        expect(response.content_type)
+          .to start_with('application/json')
+
+        expect(MediaAttachment.where(id: media.id)).to_not exist
+      end
+    end
+
+    context 'when media is attached to a status' do
+      let(:media) { Fabricate(:media_attachment, account: user.account, status: Fabricate.build(:status)) }
+
+      it 'returns http unprocessable entity' do
+        subject
+
+        expect(response).to have_http_status(422)
+        expect(response.content_type)
+          .to start_with('application/json')
+        expect(response.parsed_body).to match(
+          a_hash_including(
+            error: 'Media attachment is currently used by a status'
+          )
+        )
+
+        expect(MediaAttachment.where(id: media.id)).to exist
+      end
+    end
+
+    context 'when the media belongs to somebody else' do
+      let(:media) { Fabricate(:media_attachment, status: nil) }
+
+      it 'returns http not found' do
+        subject
+
+        expect(response).to have_http_status(404)
+        expect(response.content_type)
+          .to start_with('application/json')
+
+        expect(MediaAttachment.where(id: media.id)).to exist
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/v1/profiles_spec.rb b/spec/requests/api/v1/profiles_spec.rb
index fd3ab4bf58..de7a20b133 100644
--- a/spec/requests/api/v1/profiles_spec.rb
+++ b/spec/requests/api/v1/profiles_spec.rb
@@ -15,10 +15,6 @@ RSpec.describe 'Deleting profile images' do
   let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
 
   describe 'DELETE /api/v1/profile' do
-    before do
-      allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
-    end
-
     context 'when deleting an avatar' do
       context 'with wrong scope' do
         before do
@@ -38,7 +34,8 @@ RSpec.describe 'Deleting profile images' do
         account.reload
         expect(account.avatar).to_not exist
         expect(account.header).to exist
-        expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
+        expect(ActivityPub::UpdateDistributionWorker)
+          .to have_enqueued_sidekiq_job(account.id)
       end
     end
 
@@ -61,7 +58,8 @@ RSpec.describe 'Deleting profile images' do
         account.reload
         expect(account.avatar).to exist
         expect(account.header).to_not exist
-        expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
+        expect(ActivityPub::UpdateDistributionWorker)
+          .to have_enqueued_sidekiq_job(account.id)
       end
     end
   end
diff --git a/spec/requests/api/v1/push/subscriptions_spec.rb b/spec/requests/api/v1/push/subscriptions_spec.rb
index f2b457705e..69adeb9b6f 100644
--- a/spec/requests/api/v1/push/subscriptions_spec.rb
+++ b/spec/requests/api/v1/push/subscriptions_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe 'API V1 Push Subscriptions' do
       subscription: {
         endpoint: endpoint,
         keys: keys,
+        standard: standard,
       },
     }.with_indifferent_access
   end
@@ -36,6 +37,7 @@ RSpec.describe 'API V1 Push Subscriptions' do
       },
     }.with_indifferent_access
   end
+  let(:standard) { '1' }
   let(:scopes) { 'push' }
   let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
@@ -66,6 +68,7 @@ RSpec.describe 'API V1 Push Subscriptions' do
           user_id: eq(user.id),
           access_token_id: eq(token.id)
         )
+        .and be_standard
 
       expect(response.parsed_body.with_indifferent_access)
         .to include(
@@ -73,6 +76,17 @@ RSpec.describe 'API V1 Push Subscriptions' do
         )
     end
 
+    context 'when standard is provided as false value' do
+      let(:standard) { '0' }
+
+      it 'saves push subscription with standard as false' do
+        subject
+
+        expect(endpoint_push_subscription)
+          .to_not be_standard
+      end
+    end
+
     it 'replaces old subscription on repeat calls' do
       2.times { subject }
 
diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb
index 03fe3928c6..266f1eda5a 100644
--- a/spec/requests/api/v1/statuses_spec.rb
+++ b/spec/requests/api/v1/statuses_spec.rb
@@ -346,13 +346,30 @@ RSpec.describe '/api/v1/statuses' do
 
       it_behaves_like 'forbidden for wrong scope', 'read read:statuses'
 
-      it 'removes the status', :aggregate_failures do
+      it 'discards the status and schedules removal as a redraft', :aggregate_failures do
         subject
 
         expect(response).to have_http_status(200)
         expect(response.content_type)
           .to start_with('application/json')
         expect(Status.find_by(id: status.id)).to be_nil
+        expect(RemovalWorker).to have_enqueued_sidekiq_job(status.id, { 'redraft' => true })
+      end
+
+      context 'when called with truthy delete_media' do
+        subject do
+          delete "/api/v1/statuses/#{status.id}?delete_media=true", headers: headers
+        end
+
+        it 'discards the status and schedules removal without the redraft flag', :aggregate_failures do
+          subject
+
+          expect(response).to have_http_status(200)
+          expect(response.content_type)
+            .to start_with('application/json')
+          expect(Status.find_by(id: status.id)).to be_nil
+          expect(RemovalWorker).to have_enqueued_sidekiq_job(status.id, { 'redraft' => false })
+        end
       end
     end
 
diff --git a/spec/requests/api/v1/trends/tags_spec.rb b/spec/requests/api/v1/trends/tags_spec.rb
index 14ab73fc96..097393e58d 100644
--- a/spec/requests/api/v1/trends/tags_spec.rb
+++ b/spec/requests/api/v1/trends/tags_spec.rb
@@ -15,6 +15,8 @@ RSpec.describe 'API V1 Trends Tags' do
           .and not_have_http_link_header
         expect(response.content_type)
           .to start_with('application/json')
+        expect(response.headers['Deprecation'])
+          .to be_nil
       end
     end
 
@@ -31,6 +33,8 @@ RSpec.describe 'API V1 Trends Tags' do
           .and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next')
         expect(response.content_type)
           .to start_with('application/json')
+        expect(response.headers['Deprecation'])
+          .to be_nil
       end
 
       def prepare_trends
diff --git a/spec/requests/api/v1/trends_spec.rb b/spec/requests/api/v1/trends_spec.rb
new file mode 100644
index 0000000000..5bfabdca1c
--- /dev/null
+++ b/spec/requests/api/v1/trends_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'deprecated API V1 Trends Tags' do
+  describe 'GET /api/v1/trends' do
+    context 'when trends are disabled' do
+      before { Setting.trends = false }
+
+      it 'returns http success' do
+        get '/api/v1/trends'
+
+        expect(response)
+          .to have_http_status(200)
+          .and not_have_http_link_header
+        expect(response.content_type)
+          .to start_with('application/json')
+        expect(response.headers['Deprecation'])
+          .to start_with '@'
+      end
+    end
+
+    context 'when trends are enabled' do
+      before { Setting.trends = true }
+
+      it 'returns http success' do
+        prepare_trends
+        stub_const('Api::V1::Trends::TagsController::DEFAULT_TAGS_LIMIT', 2)
+        get '/api/v1/trends'
+
+        expect(response)
+          .to have_http_status(200)
+          .and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next')
+        expect(response.content_type)
+          .to start_with('application/json')
+        expect(response.headers['Deprecation'])
+          .to start_with '@'
+      end
+
+      def prepare_trends
+        Fabricate.times(3, :tag, trendable: true).each do |tag|
+          2.times { |i| Trends.tags.add(tag, i) }
+        end
+        Trends::Tags.new(threshold: 1).refresh
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v2/filters_spec.rb b/spec/requests/api/v2/filters_spec.rb
index cb378d662a..ddf5743346 100644
--- a/spec/requests/api/v2/filters_spec.rb
+++ b/spec/requests/api/v2/filters_spec.rb
@@ -141,6 +141,21 @@ RSpec.describe 'Filters' do
           .to start_with('application/json')
       end
     end
+
+    context 'when the given filter_action value is invalid' do
+      let(:params) { { title: 'magic', filter_action: 'imaginary_value', keywords_attributes: [keyword: 'magic'] } }
+
+      it 'returns http unprocessable entity' do
+        subject
+
+        expect(response)
+          .to have_http_status(422)
+        expect(response.content_type)
+          .to start_with('application/json')
+        expect(response.parsed_body)
+          .to include(error: /Action is not included/)
+      end
+    end
   end
 
   describe 'GET /api/v2/filters/:id' do
diff --git a/spec/requests/api/v2/notifications_spec.rb b/spec/requests/api/v2/notifications_spec.rb
index b2f6d71b51..69feb6cb6e 100644
--- a/spec/requests/api/v2/notifications_spec.rb
+++ b/spec/requests/api/v2/notifications_spec.rb
@@ -158,19 +158,18 @@ RSpec.describe 'Notifications' do
           expect(response).to have_http_status(200)
           expect(response.content_type)
             .to start_with('application/json')
-          expect(response.parsed_body[:notification_groups]).to contain_exactly(
-            a_hash_including(
-              type: 'favourite',
-              sample_account_ids: have_attributes(size: 5),
-              page_min_id: notification_ids.first.to_s,
-              page_max_id: notification_ids.last.to_s
-            )
-          )
+          expect(response.parsed_body[:notification_groups].size)
+            .to eq(1)
+          expect(response.parsed_body.dig(:notification_groups, 0))
+            .to include(type: 'favourite')
+            .and(include(sample_account_ids: have_attributes(size: 5)))
+            .and(include(page_max_id: notification_ids.last.to_s))
+            .and(include(page_min_id: notification_ids.first.to_s))
         end
       end
 
       context 'with min_id param' do
-        let(:params) { { min_id: user.account.notifications.reload.first.id - 1 } }
+        let(:params) { { min_id: user.account.notifications.order(id: :asc).first.id - 1 } }
 
         it 'returns a notification group covering all notifications' do
           subject
@@ -180,14 +179,13 @@ RSpec.describe 'Notifications' do
           expect(response).to have_http_status(200)
           expect(response.content_type)
             .to start_with('application/json')
-          expect(response.parsed_body[:notification_groups]).to contain_exactly(
-            a_hash_including(
-              type: 'favourite',
-              sample_account_ids: have_attributes(size: 5),
-              page_min_id: notification_ids.first.to_s,
-              page_max_id: notification_ids.last.to_s
-            )
-          )
+          expect(response.parsed_body[:notification_groups].size)
+            .to eq(1)
+          expect(response.parsed_body.dig(:notification_groups, 0))
+            .to include(type: 'favourite')
+            .and(include(sample_account_ids: have_attributes(size: 5)))
+            .and(include(page_max_id: notification_ids.last.to_s))
+            .and(include(page_min_id: notification_ids.first.to_s))
         end
       end
     end
@@ -365,6 +363,18 @@ RSpec.describe 'Notifications' do
         .to start_with('application/json')
     end
 
+    context 'with an ungrouped notification' do
+      let(:notification) { Fabricate(:notification, account: user.account, type: :favourite) }
+
+      it 'returns http success' do
+        get "/api/v2/notifications/ungrouped-#{notification.id}", headers: headers
+
+        expect(response).to have_http_status(200)
+        expect(response.content_type)
+          .to start_with('application/json')
+      end
+    end
+
     context 'when notification belongs to someone else' do
       let(:notification) { Fabricate(:notification, group_key: 'foobar') }
 
@@ -396,6 +406,19 @@ RSpec.describe 'Notifications' do
       expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
     end
 
+    context 'with an ungrouped notification' do
+      let(:notification) { Fabricate(:notification, account: user.account, type: :favourite) }
+
+      it 'destroys the notification' do
+        post "/api/v2/notifications/ungrouped-#{notification.id}/dismiss", headers: headers
+
+        expect(response).to have_http_status(200)
+        expect(response.content_type)
+          .to start_with('application/json')
+        expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+    end
+
     context 'when notification belongs to someone else' do
       let(:notification) { Fabricate(:notification, group_key: 'foobar') }
 
diff --git a/spec/requests/auth/passwords_spec.rb b/spec/requests/auth/passwords_spec.rb
new file mode 100644
index 0000000000..feefd94587
--- /dev/null
+++ b/spec/requests/auth/passwords_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Auth Passwords' do
+  describe 'GET /auth/password/edit' do
+    context 'with invalid reset_password_token' do
+      it 'redirects to #new' do
+        get edit_user_password_path, params: { reset_password_token: 'some_invalid_value' }
+
+        expect(response)
+          .to redirect_to new_user_password_path
+      end
+    end
+  end
+
+  describe 'PUT /auth/password' do
+    let(:user) { Fabricate(:user) }
+    let(:password) { 'reset0password' }
+
+    context 'with invalid reset_password_token' do
+      it 'renders reset password and retains password' do
+        put user_password_path, params: { user: { password: password, password_confirmation: password, reset_password_token: 'some_invalid_value' } }
+
+        expect(response.body)
+          .to include(I18n.t('auth.set_new_password'))
+
+        expect(User.find(user.id))
+          .to be_present
+          .and be_external_or_valid_password(user.password)
+      end
+    end
+  end
+end
diff --git a/spec/requests/auth/setup_spec.rb b/spec/requests/auth/setup_spec.rb
index 72413e1740..fa3c196805 100644
--- a/spec/requests/auth/setup_spec.rb
+++ b/spec/requests/auth/setup_spec.rb
@@ -24,15 +24,4 @@ RSpec.describe 'Auth Setup' do
       end
     end
   end
-
-  describe 'PUT /auth/setup' do
-    before { sign_in Fabricate(:user, confirmed_at: nil) }
-
-    it 'gracefully handles invalid nested params' do
-      put '/auth/setup?user=invalid'
-
-      expect(response)
-        .to have_http_status(400)
-    end
-  end
 end
diff --git a/spec/requests/disputes/strikes_spec.rb b/spec/requests/disputes/strikes_spec.rb
new file mode 100644
index 0000000000..48685893c2
--- /dev/null
+++ b/spec/requests/disputes/strikes_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Disputes Strikes' do
+  before { sign_in current_user }
+
+  describe 'GET /disputes/strikes/:id' do
+    let(:current_user) { Fabricate(:user) }
+
+    context 'when meant for a different user' do
+      let(:strike) { Fabricate(:account_warning) }
+
+      it 'returns http forbidden' do
+        get disputes_strike_path(strike)
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+  end
+end
diff --git a/spec/requests/filters/statuses_spec.rb b/spec/requests/filters/statuses_spec.rb
index aa1d049da7..b462b56223 100644
--- a/spec/requests/filters/statuses_spec.rb
+++ b/spec/requests/filters/statuses_spec.rb
@@ -16,4 +16,30 @@ RSpec.describe 'Filters Statuses' do
         .to redirect_to(edit_filter_path(filter))
     end
   end
+
+  describe 'GET /filters/:filter_id/statuses' do
+    let(:filter) { Fabricate(:custom_filter) }
+
+    context 'with signed out user' do
+      it 'redirects' do
+        get filter_statuses_path(filter)
+
+        expect(response)
+          .to be_redirect
+      end
+    end
+
+    context 'with a signed in user' do
+      context 'with another user signed in' do
+        before { sign_in(Fabricate(:user)) }
+
+        it 'returns http not found' do
+          get filter_statuses_path(filter)
+
+          expect(response)
+            .to have_http_status(404)
+        end
+      end
+    end
+  end
 end
diff --git a/spec/controllers/intents_controller_spec.rb b/spec/requests/intents_spec.rb
similarity index 90%
rename from spec/controllers/intents_controller_spec.rb
rename to spec/requests/intents_spec.rb
index 668d833ea7..b62f570d7a 100644
--- a/spec/controllers/intents_controller_spec.rb
+++ b/spec/requests/intents_spec.rb
@@ -2,15 +2,15 @@
 
 require 'rails_helper'
 
-RSpec.describe IntentsController do
-  render_views
-
+RSpec.describe 'Intents' do
   let(:user) { Fabricate(:user) }
 
   before { sign_in user, scope: :user }
 
-  describe 'GET #show' do
-    subject { get :show, params: { uri: uri } }
+  describe 'GET /intent' do
+    subject { response }
+
+    before { get intent_path(uri: uri) }
 
     context 'when schema is web+mastodon' do
       context 'when host is follow' do
diff --git a/spec/requests/oauth/token_spec.rb b/spec/requests/oauth/token_spec.rb
index 18d232e5ab..74f301c577 100644
--- a/spec/requests/oauth/token_spec.rb
+++ b/spec/requests/oauth/token_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-RSpec.describe 'Obtaining OAuth Tokens' do
+RSpec.describe 'Managing OAuth Tokens' do
   describe 'POST /oauth/token' do
     subject do
       post '/oauth/token', params: params
@@ -104,4 +104,23 @@ RSpec.describe 'Obtaining OAuth Tokens' do
       end
     end
   end
+
+  describe 'POST /oauth/revoke' do
+    subject { post '/oauth/revoke', params: { client_id: application.uid, token: access_token.token } }
+
+    let!(:user) { Fabricate(:user) }
+    let!(:application) { Fabricate(:application, confidential: false) }
+    let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) }
+    let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
+
+    it 'revokes the token and removes subscriptions' do
+      expect { subject }
+        .to change { access_token.reload.revoked_at }.from(nil).to(be_present)
+
+      expect(Web::PushSubscription.where(access_token: access_token).count)
+        .to eq(0)
+      expect { web_push_subscription.reload }
+        .to raise_error(ActiveRecord::RecordNotFound)
+    end
+  end
 end
diff --git a/spec/requests/settings/deletes_spec.rb b/spec/requests/settings/deletes_spec.rb
index 4563f639d5..c277181999 100644
--- a/spec/requests/settings/deletes_spec.rb
+++ b/spec/requests/settings/deletes_spec.rb
@@ -4,13 +4,65 @@ require 'rails_helper'
 
 RSpec.describe 'Settings Deletes' do
   describe 'DELETE /settings/delete' do
-    before { sign_in Fabricate(:user) }
+    context 'when signed in' do
+      before { sign_in(user) }
 
-    it 'gracefully handles invalid nested params' do
-      delete settings_delete_path(form_delete_confirmation: 'invalid')
+      let(:user) { Fabricate(:user) }
 
-      expect(response)
-        .to have_http_status(400)
+      it 'gracefully handles invalid nested params' do
+        delete settings_delete_path(form_delete_confirmation: 'invalid')
+
+        expect(response)
+          .to have_http_status(400)
+      end
+
+      context 'when suspended' do
+        let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
+
+        it 'returns http forbidden' do
+          delete settings_delete_path
+
+          expect(response)
+            .to have_http_status(403)
+        end
+      end
+    end
+
+    context 'when not signed in' do
+      it 'redirects to sign in' do
+        delete settings_delete_path
+
+        expect(response)
+          .to redirect_to(new_user_session_path)
+      end
+    end
+  end
+
+  describe 'GET /settings/delete' do
+    context 'when signed in' do
+      before { sign_in(user) }
+
+      context 'when suspended' do
+        let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
+
+        it 'returns http forbidden with private cache control headers' do
+          get settings_delete_path
+
+          expect(response)
+            .to have_http_status(403)
+          expect(response.headers['Cache-Control'])
+            .to include('private, no-store')
+        end
+      end
+    end
+
+    context 'when not signed in' do
+      it 'redirects to sign in' do
+        get settings_delete_path
+
+        expect(response)
+          .to redirect_to(new_user_session_path)
+      end
     end
   end
 end
diff --git a/spec/requests/settings/featured_tags_spec.rb b/spec/requests/settings/featured_tags_spec.rb
index 2c7f907e80..26c4950acc 100644
--- a/spec/requests/settings/featured_tags_spec.rb
+++ b/spec/requests/settings/featured_tags_spec.rb
@@ -2,15 +2,23 @@
 
 require 'rails_helper'
 
-RSpec.describe 'Settings Aliases' do
+RSpec.describe 'Settings Featured Tags' do
   describe 'POST /settings/featured_tags' do
-    before { sign_in Fabricate(:user) }
+    context 'when signed in' do
+      before { sign_in Fabricate(:user) }
 
-    it 'gracefully handles invalid nested params' do
-      post settings_featured_tags_path(featured_tag: 'invalid')
+      it 'gracefully handles invalid nested params' do
+        post settings_featured_tags_path(featured_tag: 'invalid')
 
-      expect(response)
-        .to have_http_status(400)
+        expect(response)
+          .to have_http_status(400)
+      end
+    end
+
+    context 'when not signed in' do
+      subject { post settings_featured_tags_path }
+
+      it { is_expected.to redirect_to new_user_session_path }
     end
   end
 end
diff --git a/spec/requests/settings/pictures_spec.rb b/spec/requests/settings/pictures_spec.rb
new file mode 100644
index 0000000000..f297eb3649
--- /dev/null
+++ b/spec/requests/settings/pictures_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings Pictures' do
+  let!(:user) { Fabricate(:user) }
+
+  before { sign_in user }
+
+  describe 'DELETE /settings/profile/pictures/:id' do
+    context 'with invalid picture id' do
+      it 'returns http bad request' do
+        delete settings_profile_picture_path(id: 'invalid')
+
+        expect(response)
+          .to have_http_status(400)
+      end
+    end
+
+    context 'with valid picture id' do
+      before { stub_service }
+
+      context 'when account updates correctly' do
+        let(:service) { instance_double(UpdateAccountService, call: true) }
+
+        it 'updates the account' do
+          delete settings_profile_picture_path(id: 'avatar')
+
+          expect(response)
+            .to redirect_to(settings_profile_path)
+            .and have_http_status(303)
+          expect(service)
+            .to have_received(:call).with(user.account, { 'avatar' => nil, 'avatar_remote_url' => '' })
+        end
+      end
+
+      context 'when account cannot update' do
+        let(:service) { instance_double(UpdateAccountService, call: false) }
+
+        it 'redirects to profile' do
+          delete settings_profile_picture_path(id: 'avatar')
+
+          expect(response)
+            .to redirect_to(settings_profile_path)
+        end
+      end
+
+      def stub_service
+        allow(UpdateAccountService)
+          .to receive(:new)
+          .and_return(service)
+      end
+    end
+  end
+end
diff --git a/spec/requests/settings/sessions_spec.rb b/spec/requests/settings/sessions_spec.rb
new file mode 100644
index 0000000000..6a23eac4b4
--- /dev/null
+++ b/spec/requests/settings/sessions_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings Sessions' do
+  let(:user) { Fabricate(:user) }
+
+  before { sign_in(user) }
+
+  describe 'DELETE /settings/sessions/:id' do
+    context 'when session activation does not exist' do
+      it 'returns not found' do
+        delete settings_session_path(123_456_789)
+
+        expect(response)
+          .to have_http_status(404)
+      end
+    end
+  end
+end
diff --git a/spec/requests/settings/two_factor_authentication/confirmations_spec.rb b/spec/requests/settings/two_factor_authentication/confirmations_spec.rb
index bf443a5e62..532fbe61ea 100644
--- a/spec/requests/settings/two_factor_authentication/confirmations_spec.rb
+++ b/spec/requests/settings/two_factor_authentication/confirmations_spec.rb
@@ -16,4 +16,20 @@ RSpec.describe 'Settings 2FA Confirmations' do
         .to have_http_status(400)
     end
   end
+
+  context 'when not signed in' do
+    it 'redirects on POST to create' do
+      post settings_two_factor_authentication_confirmation_path(form_two_factor_confirmation: { otp_attempt: '123456' })
+
+      expect(response)
+        .to redirect_to(new_user_session_path)
+    end
+
+    it 'redirects on GET to new' do
+      get new_settings_two_factor_authentication_confirmation_path
+
+      expect(response)
+        .to redirect_to(new_user_session_path)
+    end
+  end
 end
diff --git a/spec/requests/settings/two_factor_authentication/recovery_codes_spec.rb b/spec/requests/settings/two_factor_authentication/recovery_codes_spec.rb
new file mode 100644
index 0000000000..30cbfc2a7b
--- /dev/null
+++ b/spec/requests/settings/two_factor_authentication/recovery_codes_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings TwoFactorAuthentication RecoveryCodes' do
+  describe 'POST /settings/two_factor_authentication/recovery_codes' do
+    context 'when signed out' do
+      it 'redirects to sign in page' do
+        post settings_two_factor_authentication_recovery_codes_path
+
+        expect(response)
+          .to redirect_to(new_user_session_path)
+      end
+    end
+  end
+end
diff --git a/spec/requests/settings/two_factor_authentication_methods_spec.rb b/spec/requests/settings/two_factor_authentication_methods_spec.rb
new file mode 100644
index 0000000000..2fda5ce919
--- /dev/null
+++ b/spec/requests/settings/two_factor_authentication_methods_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings TwoFactorAuthenticationMethods' do
+  context 'when not signed in' do
+    describe 'GET to /settings/two_factor_authentication_methods' do
+      it 'redirects to sign in page' do
+        get settings_two_factor_authentication_methods_path
+
+        expect(response)
+          .to redirect_to(new_user_session_path)
+      end
+    end
+  end
+
+  context 'when signed in' do
+    let(:user) { Fabricate(:user) }
+
+    before { sign_in user }
+
+    describe 'GET to /settings/two_factor_authentication_methods' do
+      describe 'when user has not enabled otp' do
+        before { user.update(otp_required_for_login: false) }
+
+        it 'redirects to enable otp' do
+          get settings_two_factor_authentication_methods_path
+
+          expect(response)
+            .to redirect_to(settings_otp_authentication_path)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/requests/status_show_page_spec.rb b/spec/requests/status_show_page_spec.rb
new file mode 100644
index 0000000000..758f9dcfd8
--- /dev/null
+++ b/spec/requests/status_show_page_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Statuses' do
+  describe 'GET /@:account_username/:id' do
+    include AccountsHelper
+
+    def site_hostname
+      Rails.configuration.x.web_domain || Rails.configuration.x.local_domain
+    end
+
+    it 'has valid opengraph tags' do
+      account = Fabricate(:account, username: 'alice', display_name: 'Alice')
+      status = Fabricate(:status, account: account, text: 'Hello World')
+
+      get "/@#{account.username}/#{status.id}"
+
+      expect(head_link_icons.size).to eq(3) # Three favicons with sizes
+
+      expect(head_meta_content('og:title')).to match "#{display_name(account)} (#{acct(account)})"
+      expect(head_meta_content('og:type')).to eq 'article'
+      expect(head_meta_content('og:published_time')).to eq status.created_at.iso8601
+      expect(head_meta_content('og:url')).to eq short_account_status_url(account_username: account.username, id: status.id)
+      expect(head_meta_exists('og:locale')).to be false
+    end
+
+    it 'has og:locale opengraph tag if the status has is written in a given language' do
+      status_text = "Una prova d'estatus català"
+      account = Fabricate(:account, username: 'alice', display_name: 'Alice')
+      status = Fabricate(:status, account: account, text: status_text, language: 'ca')
+
+      get "/@#{account.username}/#{status.id}"
+
+      expect(head_meta_content('og:title')).to match "#{display_name(account)} (#{acct(account)})"
+      expect(head_meta_content('og:type')).to eq 'article'
+      expect(head_meta_content('og:published_time')).to eq status.created_at.iso8601
+      expect(head_meta_content('og:url')).to eq short_account_status_url(account_username: account.username, id: status.id)
+
+      expect(head_meta_exists('og:locale')).to be true
+      expect(head_meta_content('og:locale')).to eq 'ca'
+      expect(head_meta_content('og:description')).to eq status_text
+    end
+
+    def head_link_icons
+      response
+        .parsed_body
+        .search('html head link[rel=icon]')
+    end
+
+    def head_meta_content(property)
+      response
+        .parsed_body
+        .search("html head meta[property='#{property}']")
+        .attr('content')
+        .text
+    end
+
+    def head_meta_exists(property)
+      !response
+        .parsed_body
+        .search("html head meta[property='#{property}']")
+        .empty?
+    end
+  end
+end
diff --git a/spec/requests/statuses_spec.rb b/spec/requests/statuses_spec.rb
index f86d67d405..a5e4482dfa 100644
--- a/spec/requests/statuses_spec.rb
+++ b/spec/requests/statuses_spec.rb
@@ -45,12 +45,79 @@ RSpec.describe 'Statuses' do
             .to redirect_to(original_status.url)
         end
       end
+
+      context 'when status visibility is public' do
+        subject { get short_account_status_path(account_username: account.username, id: status.id, format: format) }
+
+        let(:status) { Fabricate(:status, account: account, visibility: :public) }
+
+        context 'with HTML' do
+          let(:format) { 'html' }
+
+          it 'renders status successfully', :aggregate_failures do
+            subject
+
+            expect(response)
+              .to have_http_status(200)
+            expect(response.headers).to include(
+              'Vary' => 'Accept, Accept-Language, Cookie',
+              'Cache-Control' => include('public'),
+              'Link' => include('activity+json')
+            )
+            expect(response.body)
+              .to include(status.text)
+          end
+        end
+
+        context 'with JSON' do
+          let(:format) { 'json' }
+
+          it 'renders ActivityPub Note object successfully', :aggregate_failures do
+            subject
+
+            expect(response)
+              .to have_http_status(200)
+              .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
+
+            expect(response.headers).to include(
+              'Content-Type' => include('application/activity+json'),
+              'Link' => include('activity+json')
+            )
+            expect(response.parsed_body)
+              .to include(content: include(status.text))
+          end
+        end
+      end
+
+      context 'when status visibility is private' do
+        let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+        it 'returns http not found' do
+          get short_account_status_path(account_username: account.username, id: status.id)
+
+          expect(response)
+            .to have_http_status(404)
+        end
+      end
+
+      context 'when status visibility is direct' do
+        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
+
+        it 'returns http not found' do
+          get short_account_status_path(account_username: account.username, id: status.id)
+
+          expect(response)
+            .to have_http_status(404)
+        end
+      end
     end
 
     context 'when signed in' do
+      subject { get short_account_status_path(account_username: account.username, id: status.id, format: format) }
+
       let(:user) { Fabricate(:user) }
 
-      before { sign_in(user) }
+      before { sign_in_with_session(user) }
 
       context 'when account blocks user' do
         before { account.block!(user.account) }
@@ -62,6 +129,347 @@ RSpec.describe 'Statuses' do
             .to have_http_status(404)
         end
       end
+
+      context 'when status is public' do
+        context 'with HTML' do
+          let(:format) { 'html' }
+
+          it 'renders status successfully', :aggregate_failures do
+            subject
+
+            expect(response)
+              .to have_http_status(200)
+            expect(response.headers).to include(
+              'Vary' => 'Accept, Accept-Language, Cookie',
+              'Cache-Control' => include('private'),
+              'Link' => include('activity+json')
+            )
+            expect(response.body)
+              .to include(status.text)
+          end
+        end
+
+        context 'with JSON' do
+          let(:format) { 'json' }
+
+          it 'renders ActivityPub Note object successfully', :aggregate_failures do
+            subject
+
+            expect(response)
+              .to have_http_status(200)
+            expect(response.headers).to include(
+              'Vary' => 'Accept, Accept-Language, Cookie',
+              'Cache-Control' => include('private'),
+              'Content-Type' => include('application/activity+json'),
+              'Link' => include('activity+json')
+            )
+            expect(response.parsed_body)
+              .to include(content: include(status.text))
+          end
+        end
+      end
+
+      context 'when status is private' do
+        let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+        context 'when user is authorized to see it' do
+          before { user.account.follow!(account) }
+
+          context 'with HTML' do
+            let(:format) { 'html' }
+
+            it 'renders status successfully', :aggregate_failures do
+              subject
+
+              expect(response)
+                .to have_http_status(200)
+
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Link' => include('activity+json')
+              )
+              expect(response.body)
+                .to include(status.text)
+            end
+          end
+
+          context 'with JSON' do
+            let(:format) { 'json' }
+
+            it 'renders ActivityPub Note object successfully', :aggregate_failures do
+              subject
+
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Content-Type' => include('application/activity+json'),
+                'Link' => include('activity+json')
+              )
+              expect(response.parsed_body)
+                .to include(content: include(status.text))
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          let(:format) { 'html' }
+
+          it 'returns http not found' do
+            subject
+
+            expect(response)
+              .to have_http_status(404)
+          end
+        end
+      end
+
+      context 'when status is direct' do
+        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
+
+        context 'when user is authorized to see it' do
+          before { Fabricate(:mention, account: user.account, status: status) }
+
+          context 'with HTML' do
+            let(:format) { 'html' }
+
+            it 'renders status successfully', :aggregate_failures do
+              subject
+
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Link' => include('activity+json')
+              )
+              expect(response.body)
+                .to include(status.text)
+            end
+          end
+
+          context 'with JSON' do
+            let(:format) { 'json' }
+
+            it 'renders ActivityPub Note object successfully' do
+              subject
+
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Content-Type' => include('application/activity+json'),
+                'Link' => include('activity+json')
+              )
+              expect(response.parsed_body)
+                .to include(content: include(status.text))
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          let(:format) { 'html' }
+
+          it 'returns http not found' do
+            subject
+
+            expect(response)
+              .to have_http_status(404)
+          end
+        end
+      end
+
+      private
+
+      def sign_in_with_session(user)
+        # The regular `sign_in` helper does not actually set session cookies
+        # The endpoint responses here rely on cookie/session checks to set cache privacy headers
+        # To enable that, perform a full sign in which will establish those cookies for subsequent spec requests
+        post user_session_path, params: { user: { email: user.email, password: user.password } }
+      end
+    end
+
+    context 'with "HTTP Signature" access signed by a remote account' do
+      subject do
+        get short_account_status_path(account_username: status.account.username, id: status.id, format: format),
+            headers: nil,
+            sign_with: remote_account
+      end
+
+      let(:format) { 'html' }
+      let(:remote_account) { Fabricate(:account, domain: 'host.example') }
+
+      context 'when account blocks the remote account' do
+        before { account.block!(remote_account) }
+
+        it 'returns http not found' do
+          subject
+
+          expect(response)
+            .to have_http_status(404)
+        end
+      end
+
+      context 'when account domain blocks the domain of the remote account' do
+        before { account.block_domain!(remote_account.domain) }
+
+        it 'returns http not found' do
+          subject
+
+          expect(response)
+            .to have_http_status(404)
+        end
+      end
+
+      context 'when status has public visibility' do
+        context 'with HTML' do
+          let(:format) { 'html' }
+
+          it 'renders status successfully', :aggregate_failures do
+            subject
+
+            expect(response)
+              .to have_http_status(200)
+            expect(response.headers).to include(
+              'Vary' => 'Accept, Accept-Language, Cookie',
+              'Cache-Control' => include('private'),
+              'Link' => include('activity+json')
+            )
+            expect(response.body)
+              .to include(status.text)
+          end
+        end
+
+        context 'with JSON' do
+          let(:format) { 'json' }
+
+          it 'renders ActivityPub Note object successfully', :aggregate_failures do
+            subject
+
+            expect(response)
+              .to have_http_status(200)
+              .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
+            expect(response.headers).to include(
+              'Content-Type' => include('application/activity+json'),
+              'Link' => include('activity+json')
+            )
+            expect(response.parsed_body)
+              .to include(content: include(status.text))
+          end
+        end
+      end
+
+      context 'when status has private visibility' do
+        let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+        context 'when user is authorized to see it' do
+          before { remote_account.follow!(account) }
+
+          context 'with HTML' do
+            let(:format) { 'html' }
+
+            it 'renders status successfully', :aggregate_failures do
+              subject
+
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Link' => include('activity+json')
+              )
+              expect(response.body)
+                .to include(status.text)
+            end
+          end
+
+          context 'with JSON' do
+            let(:format) { 'json' }
+
+            it 'renders ActivityPub Note object successfully' do
+              subject
+
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Content-Type' => include('application/activity+json'),
+                'Link' => include('activity+json')
+              )
+
+              expect(response.parsed_body)
+                .to include(content: include(status.text))
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          it 'returns http not found' do
+            subject
+
+            expect(response)
+              .to have_http_status(404)
+          end
+        end
+      end
+
+      context 'when status is direct' do
+        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
+
+        context 'when user is authorized to see it' do
+          before { Fabricate(:mention, account: remote_account, status: status) }
+
+          context 'with HTML' do
+            let(:format) { 'html' }
+
+            it 'renders status successfully', :aggregate_failures do
+              subject
+
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Link' => include('activity+json')
+              )
+              expect(response.body)
+                .to include(status.text)
+            end
+          end
+
+          context 'with JSON' do
+            let(:format) { 'json' }
+
+            it 'renders ActivityPub Note object', :aggregate_failures do
+              subject
+
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Content-Type' => include('application/activity+json'),
+                'Link' => include('activity+json')
+              )
+              expect(response.parsed_body)
+                .to include(content: include(status.text))
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          it 'returns http not found' do
+            subject
+
+            expect(response)
+              .to have_http_status(404)
+          end
+        end
+      end
     end
   end
 end
diff --git a/spec/requests/well_known/webfinger_spec.rb b/spec/requests/well_known/webfinger_spec.rb
index aeff56aebf..b4aeb65320 100644
--- a/spec/requests/well_known/webfinger_spec.rb
+++ b/spec/requests/well_known/webfinger_spec.rb
@@ -116,19 +116,13 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
       perform_request!
     end
 
-    it 'returns http success' do
+    it 'returns http success with expect headers and media type' do
       expect(response).to have_http_status(200)
-    end
 
-    it 'sets only a Vary Origin header' do
       expect(response.headers['Vary']).to eq('Origin')
-    end
 
-    it 'returns application/jrd+json' do
       expect(response.media_type).to eq 'application/jrd+json'
-    end
 
-    it 'returns links for the internal account' do
       expect(response.parsed_body)
         .to include(
           subject: 'acct:mastodon.internal@cb6e6126.ngrok.io',
diff --git a/spec/services/activitypub/fetch_all_replies_service_spec.rb b/spec/services/activitypub/fetch_all_replies_service_spec.rb
new file mode 100644
index 0000000000..241c1a8464
--- /dev/null
+++ b/spec/services/activitypub/fetch_all_replies_service_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ActivityPub::FetchAllRepliesService do
+  subject { described_class.new }
+
+  let(:actor)          { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
+  let(:status)         { Fabricate(:status, account: actor) }
+  let(:collection_uri) { 'http://example.com/replies/1' }
+
+  let(:items) do
+    %w(
+      http://example.com/self-reply-1
+      http://example.com/self-reply-2
+      http://example.com/self-reply-3
+      http://other.com/other-reply-1
+      http://other.com/other-reply-2
+      http://other.com/other-reply-3
+      http://example.com/self-reply-4
+      http://example.com/self-reply-5
+      http://example.com/self-reply-6
+    )
+  end
+
+  let(:payload) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      type: 'Collection',
+      id: collection_uri,
+      items: items,
+    }.with_indifferent_access
+  end
+
+  describe '#call' do
+    it 'fetches more than the default maximum and from multiple domains' do
+      allow(FetchReplyWorker).to receive(:push_bulk)
+
+      subject.call(status.uri, payload)
+
+      expect(FetchReplyWorker).to have_received(:push_bulk).with(
+        %w(
+          http://example.com/self-reply-1
+          http://example.com/self-reply-2
+          http://example.com/self-reply-3
+          http://other.com/other-reply-1
+          http://other.com/other-reply-2
+          http://other.com/other-reply-3
+          http://example.com/self-reply-4
+          http://example.com/self-reply-5
+          http://example.com/self-reply-6
+        )
+      )
+    end
+
+    context 'with a recent status' do
+      before do
+        Fabricate(:status, uri: 'http://example.com/self-reply-2', fetched_replies_at: 1.second.ago, local: false)
+      end
+
+      it 'skips statuses that have been updated recently' do
+        allow(FetchReplyWorker).to receive(:push_bulk)
+
+        subject.call(status.uri, payload)
+
+        expect(FetchReplyWorker).to have_received(:push_bulk).with(
+          %w(
+            http://example.com/self-reply-1
+            http://example.com/self-reply-3
+            http://other.com/other-reply-1
+            http://other.com/other-reply-2
+            http://other.com/other-reply-3
+            http://example.com/self-reply-4
+            http://example.com/self-reply-5
+            http://example.com/self-reply-6
+          )
+        )
+      end
+    end
+
+    context 'with an old status' do
+      before do
+        Fabricate(:status, uri: 'http://other.com/other-reply-1', fetched_replies_at: 1.year.ago, created_at: 1.year.ago, account: actor)
+      end
+
+      it 'updates the time that fetched statuses were last fetched' do
+        allow(FetchReplyWorker).to receive(:push_bulk)
+
+        subject.call(status.uri, payload)
+
+        expect(Status.find_by(uri: 'http://other.com/other-reply-1').fetched_replies_at).to be >= 1.minute.ago
+      end
+    end
+
+    context 'with unsubscribed replies' do
+      before do
+        remote_actor = Fabricate(:account, domain: 'other.com', uri: 'http://other.com/account')
+        # reply not in the collection from the remote instance, but we know about anyway without anyone following the account
+        Fabricate(:status, account: remote_actor, in_reply_to_id: status.id, uri: 'http://other.com/account/unsubscribed', fetched_replies_at: 1.year.ago, created_at: 1.year.ago)
+      end
+
+      it 'updates the unsubscribed replies' do
+        allow(FetchReplyWorker).to receive(:push_bulk)
+
+        subject.call(status.uri, payload)
+
+        expect(FetchReplyWorker).to have_received(:push_bulk).with(
+          %w(
+            http://example.com/self-reply-1
+            http://example.com/self-reply-2
+            http://example.com/self-reply-3
+            http://other.com/other-reply-1
+            http://other.com/other-reply-2
+            http://other.com/other-reply-3
+            http://example.com/self-reply-4
+            http://example.com/self-reply-5
+            http://example.com/self-reply-6
+            http://other.com/account/unsubscribed
+          )
+        )
+      end
+    end
+  end
+end
diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb
index 9d8c6e0e0a..2503a58ac2 100644
--- a/spec/services/activitypub/fetch_remote_status_service_spec.rb
+++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb
@@ -9,6 +9,9 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
 
   let!(:sender) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar') }
 
+  let(:follower) { Fabricate(:account, username: 'alice') }
+  let(:follow) { nil }
+  let(:response) { { body: Oj.dump(object), headers: { 'content-type': 'application/activity+json' } } }
   let(:existing_status) { nil }
 
   let(:note) do
@@ -23,13 +26,14 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
 
   before do
     stub_request(:get, 'https://foo.bar/watch?v=12345').to_return(status: 404, body: '')
-    stub_request(:get, object[:id]).to_return(body: Oj.dump(object))
+    stub_request(:get, object[:id]).to_return(**response)
   end
 
   describe '#call' do
     before do
+      follow
       existing_status
-      subject.call(object[:id], prefetched_body: Oj.dump(object))
+      subject.call(object[:id])
     end
 
     context 'with Note object' do
@@ -254,6 +258,45 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
           expect(existing_status.text).to eq 'Lorem ipsum'
           expect(existing_status.edits).to_not be_empty
         end
+
+        context 'when the status appears to have been deleted at source' do
+          let(:response) { { status: 404, body: '' } }
+
+          shared_examples 'no delete' do
+            it 'does not delete the status' do
+              existing_status.reload
+              expect(existing_status.text).to eq 'Foo'
+              expect(existing_status.edits).to be_empty
+            end
+          end
+
+          context 'when the status is orphaned/unsubscribed' do
+            it 'deletes the orphaned status' do
+              expect { existing_status.reload }.to raise_error(ActiveRecord::RecordNotFound)
+            end
+          end
+
+          context 'when the status is from an account with only remote followers' do
+            let(:follower) { Fabricate(:account, username: 'alice', domain: 'foo.bar') }
+            let(:follow) { Fabricate(:follow, account: follower, target_account: sender, created_at: 2.days.ago) }
+
+            it 'deletes the orphaned status' do
+              expect { existing_status.reload }.to raise_error(ActiveRecord::RecordNotFound)
+            end
+
+            context 'when the status is private' do
+              let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id], visibility: :private) }
+
+              it_behaves_like 'no delete'
+            end
+
+            context 'when the status is direct' do
+              let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id], visibility: :direct) }
+
+              it_behaves_like 'no delete'
+            end
+          end
+        end
       end
 
       context 'with a Create activity' do
diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb
index e7d8d3528a..36159309f1 100644
--- a/spec/services/activitypub/fetch_replies_service_spec.rb
+++ b/spec/services/activitypub/fetch_replies_service_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
         it 'queues the expected worker' do
           allow(FetchReplyWorker).to receive(:push_bulk)
 
-          subject.call(status, payload)
+          subject.call(status.account.uri, payload)
 
           expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1'])
         end
@@ -50,7 +50,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
         it 'spawns workers for up to 5 replies on the same server' do
           allow(FetchReplyWorker).to receive(:push_bulk)
 
-          subject.call(status, payload)
+          subject.call(status.account.uri, payload)
 
           expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
         end
@@ -64,7 +64,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
         it 'spawns workers for up to 5 replies on the same server' do
           allow(FetchReplyWorker).to receive(:push_bulk)
 
-          subject.call(status, collection_uri)
+          subject.call(status.account.uri, collection_uri)
 
           expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
         end
@@ -85,7 +85,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
         it 'spawns workers for up to 5 replies on the same server' do
           allow(FetchReplyWorker).to receive(:push_bulk)
 
-          subject.call(status, payload)
+          subject.call(status.account.uri, payload)
 
           expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
         end
@@ -99,7 +99,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
         it 'spawns workers for up to 5 replies on the same server' do
           allow(FetchReplyWorker).to receive(:push_bulk)
 
-          subject.call(status, collection_uri)
+          subject.call(status.account.uri, collection_uri)
 
           expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
         end
@@ -124,7 +124,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
         it 'spawns workers for up to 5 replies on the same server' do
           allow(FetchReplyWorker).to receive(:push_bulk)
 
-          subject.call(status, payload)
+          subject.call(status.account.uri, payload)
 
           expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
         end
@@ -138,7 +138,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
         it 'spawns workers for up to 5 replies on the same server' do
           allow(FetchReplyWorker).to receive(:push_bulk)
 
-          subject.call(status, collection_uri)
+          subject.call(status.account.uri, collection_uri)
 
           expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
         end
diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb
index 49d496e295..6d008840be 100644
--- a/spec/services/activitypub/process_status_update_service_spec.rb
+++ b/spec/services/activitypub/process_status_update_service_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
     [
       { type: 'Hashtag', name: 'hoge' },
       { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
+      { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
       { type: 'Mention', href: bogus_mention },
     ]
   end
diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb
index 974368b7d7..70f27627e1 100644
--- a/spec/services/activitypub/synchronize_followers_service_spec.rb
+++ b/spec/services/activitypub/synchronize_followers_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
   let(:bob)            { Fabricate(:account, username: 'bob') }
   let(:eve)            { Fabricate(:account, username: 'eve') }
   let(:mallory)        { Fabricate(:account, username: 'mallory') }
-  let(:collection_uri) { 'http://example.com/partial-followers' }
+  let(:collection_uri) { 'https://example.com/partial-followers' }
 
   let(:items) do
     [alice, eve, mallory].map do |account|
@@ -27,14 +27,14 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
     }.with_indifferent_access
   end
 
+  before do
+    alice.follow!(actor)
+    bob.follow!(actor)
+    mallory.request_follow!(actor)
+  end
+
   shared_examples 'synchronizes followers' do
     before do
-      alice.follow!(actor)
-      bob.follow!(actor)
-      mallory.request_follow!(actor)
-
-      allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
-
       subject.call(actor, collection_uri)
     end
 
@@ -46,7 +46,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
       expect(mallory)
         .to be_following(actor) # Convert follow request to follow when accepted
       expect(ActivityPub::DeliveryWorker)
-        .to have_received(:perform_async).with(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
+        .to have_enqueued_sidekiq_job(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
     end
   end
 
@@ -76,7 +76,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
       it_behaves_like 'synchronizes followers'
     end
 
-    context 'when the endpoint is a paginated Collection of actor URIs' do
+    context 'when the endpoint is a single-page paginated Collection of actor URIs' do
       let(:payload) do
         {
           '@context': 'https://www.w3.org/ns/activitystreams',
@@ -96,5 +96,106 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
 
       it_behaves_like 'synchronizes followers'
     end
+
+    context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do
+      before do
+        stub_request(:get, 'https://example.com/partial-followers')
+          .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+            '@context': 'https://www.w3.org/ns/activitystreams',
+            type: 'Collection',
+            id: 'https://example.com/partial-followers',
+            first: 'https://example.com/partial-followers/1',
+          }))
+
+        stub_request(:get, 'https://example.com/partial-followers/1')
+          .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+            '@context': 'https://www.w3.org/ns/activitystreams',
+            type: 'CollectionPage',
+            id: 'https://example.com/partial-followers/1',
+            partOf: 'https://example.com/partial-followers',
+            next: 'https://example.com/partial-followers/2',
+            items: [alice, eve].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
+          }))
+
+        stub_request(:get, 'https://example.com/partial-followers/2')
+          .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+            '@context': 'https://www.w3.org/ns/activitystreams',
+            type: 'CollectionPage',
+            id: 'https://example.com/partial-followers/2',
+            partOf: 'https://example.com/partial-followers',
+            items: ActivityPub::TagManager.instance.uri_for(mallory),
+          }))
+      end
+
+      it_behaves_like 'synchronizes followers'
+    end
+
+    context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do
+      before do
+        stub_request(:get, 'https://example.com/partial-followers')
+          .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+            '@context': 'https://www.w3.org/ns/activitystreams',
+            type: 'Collection',
+            id: 'https://example.com/partial-followers',
+            first: 'https://example.com/partial-followers/1',
+          }))
+
+        stub_request(:get, 'https://example.com/partial-followers/1')
+          .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+            '@context': 'https://www.w3.org/ns/activitystreams',
+            type: 'CollectionPage',
+            id: 'https://example.com/partial-followers/1',
+            partOf: 'https://example.com/partial-followers',
+            next: 'https://example.com/partial-followers/2',
+            items: [mallory].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
+          }))
+
+        stub_request(:get, 'https://example.com/partial-followers/2')
+          .to_return(status: 404)
+      end
+
+      it 'confirms pending follow request but does not remove extra followers' do
+        previous_follower_ids = actor.followers.pluck(:id)
+
+        subject.call(actor, collection_uri)
+
+        expect(previous_follower_ids - actor.followers.reload.pluck(:id))
+          .to be_empty
+        expect(mallory)
+          .to be_following(actor)
+      end
+    end
+
+    context 'when the endpoint is a paginated Collection of actor URIs with more pages than we allow' do
+      let(:payload) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          type: 'Collection',
+          id: collection_uri,
+          first: {
+            type: 'CollectionPage',
+            partOf: collection_uri,
+            items: items,
+            next: "#{collection_uri}/page2",
+          },
+        }.with_indifferent_access
+      end
+
+      before do
+        stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1)
+        stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
+      end
+
+      it 'confirms pending follow request but does not remove extra followers' do
+        previous_follower_ids = actor.followers.pluck(:id)
+
+        subject.call(actor, collection_uri)
+
+        expect(previous_follower_ids - actor.followers.reload.pluck(:id))
+          .to be_empty
+        expect(mallory)
+          .to be_following(actor)
+      end
+    end
   end
 end
diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb
deleted file mode 100644
index 2e1358c635..0000000000
--- a/spec/services/import_service_spec.rb
+++ /dev/null
@@ -1,242 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe ImportService, :inline_jobs do
-  include RoutingHelper
-
-  let!(:account) { Fabricate(:account, locked: false) }
-  let!(:bob)     { Fabricate(:account, username: 'bob', locked: false) }
-  let!(:eve)     { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') }
-
-  before do
-    stub_request(:post, 'https://example.com/inbox').to_return(status: 200)
-  end
-
-  context 'when importing old-style list of muted users' do
-    subject { described_class.new }
-
-    let(:csv) { attachment_fixture('mute-imports.txt') }
-
-    describe 'when no accounts are muted' do
-      let(:import) { Import.create(account: account, type: 'muting', data: csv) }
-
-      it 'mutes the listed accounts, including notifications' do
-        subject.call(import)
-        expect(account.muting.count).to eq 2
-        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
-      end
-    end
-
-    describe 'when some accounts are muted and overwrite is not set' do
-      let(:import) { Import.create(account: account, type: 'muting', data: csv) }
-
-      it 'mutes the listed accounts, including notifications' do
-        account.mute!(bob, notifications: false)
-        subject.call(import)
-        expect(account.muting.count).to eq 2
-        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
-      end
-    end
-
-    describe 'when some accounts are muted and overwrite is set' do
-      let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
-
-      it 'mutes the listed accounts, including notifications' do
-        account.mute!(bob, notifications: false)
-        subject.call(import)
-        expect(account.muting.count).to eq 2
-        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
-      end
-    end
-  end
-
-  context 'when importing new-style list of muted users' do
-    subject { described_class.new }
-
-    let(:csv) { attachment_fixture('new-mute-imports.txt') }
-
-    describe 'when no accounts are muted' do
-      let(:import) { Import.create(account: account, type: 'muting', data: csv) }
-
-      it 'mutes the listed accounts, respecting notifications' do
-        subject.call(import)
-        expect(account.muting.count).to eq 2
-        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
-        expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
-      end
-    end
-
-    describe 'when some accounts are muted and overwrite is not set' do
-      let(:import) { Import.create(account: account, type: 'muting', data: csv) }
-
-      it 'mutes the listed accounts, respecting notifications' do
-        account.mute!(bob, notifications: true)
-        subject.call(import)
-        expect(account.muting.count).to eq 2
-        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
-        expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
-      end
-    end
-
-    describe 'when some accounts are muted and overwrite is set' do
-      let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
-
-      it 'mutes the listed accounts, respecting notifications' do
-        account.mute!(bob, notifications: true)
-        subject.call(import)
-        expect(account.muting.count).to eq 2
-        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
-        expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
-      end
-    end
-  end
-
-  context 'when importing old-style list of followed users' do
-    subject { described_class.new }
-
-    let(:csv) { attachment_fixture('mute-imports.txt') }
-
-    describe 'when no accounts are followed' do
-      let(:import) { Import.create(account: account, type: 'following', data: csv) }
-
-      it 'follows the listed accounts, including boosts' do
-        subject.call(import)
-
-        expect(account.following.count).to eq 1
-        expect(account.follow_requests.count).to eq 1
-        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
-      end
-    end
-
-    describe 'when some accounts are already followed and overwrite is not set' do
-      let(:import) { Import.create(account: account, type: 'following', data: csv) }
-
-      it 'follows the listed accounts, including notifications' do
-        account.follow!(bob, reblogs: false)
-        subject.call(import)
-        expect(account.following.count).to eq 1
-        expect(account.follow_requests.count).to eq 1
-        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
-      end
-    end
-
-    describe 'when some accounts are already followed and overwrite is set' do
-      let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
-
-      it 'mutes the listed accounts, including notifications' do
-        account.follow!(bob, reblogs: false)
-        subject.call(import)
-        expect(account.following.count).to eq 1
-        expect(account.follow_requests.count).to eq 1
-        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
-      end
-    end
-  end
-
-  context 'when importing new-style list of followed users' do
-    subject { described_class.new }
-
-    let(:csv) { attachment_fixture('new-following-imports.txt') }
-
-    describe 'when no accounts are followed' do
-      let(:import) { Import.create(account: account, type: 'following', data: csv) }
-
-      it 'follows the listed accounts, respecting boosts' do
-        subject.call(import)
-        expect(account.following.count).to eq 1
-        expect(account.follow_requests.count).to eq 1
-        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
-        expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
-      end
-    end
-
-    describe 'when some accounts are already followed and overwrite is not set' do
-      let(:import) { Import.create(account: account, type: 'following', data: csv) }
-
-      it 'mutes the listed accounts, respecting notifications' do
-        account.follow!(bob, reblogs: true)
-        subject.call(import)
-        expect(account.following.count).to eq 1
-        expect(account.follow_requests.count).to eq 1
-        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
-        expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
-      end
-    end
-
-    describe 'when some accounts are already followed and overwrite is set' do
-      let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
-
-      it 'mutes the listed accounts, respecting notifications' do
-        account.follow!(bob, reblogs: true)
-        subject.call(import)
-        expect(account.following.count).to eq 1
-        expect(account.follow_requests.count).to eq 1
-        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
-        expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
-      end
-    end
-  end
-
-  # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
-  #
-  # https://github.com/mastodon/mastodon/issues/20571
-  context 'with a utf-8 encoded domains' do
-    subject { described_class.new }
-
-    let!(:nare) { Fabricate(:account, username: 'nare', domain: 'թութ.հայ', locked: false, protocol: :activitypub, inbox_url: 'https://թութ.հայ/inbox') }
-    let(:csv) { attachment_fixture('utf8-followers.txt') }
-    let(:import) { Import.create(account: account, type: 'following', data: csv) }
-
-    # Make sure to not actually go to the remote server
-    before do
-      stub_request(:post, nare.inbox_url).to_return(status: 200)
-    end
-
-    it 'follows the listed account' do
-      expect(account.follow_requests.count).to eq 0
-      subject.call(import)
-      expect(account.follow_requests.count).to eq 1
-    end
-  end
-
-  context 'when importing bookmarks' do
-    subject { described_class.new }
-
-    let(:csv) { attachment_fixture('bookmark-imports.txt') }
-    let(:local_account)  { Fabricate(:account, username: 'foo', domain: nil) }
-    let!(:remote_status) { Fabricate(:status, uri: 'https://example.com/statuses/1312') }
-    let!(:direct_status) { Fabricate(:status, uri: 'https://example.com/statuses/direct', visibility: :direct) }
-
-    around do |example|
-      local_before = Rails.configuration.x.local_domain
-      web_before = Rails.configuration.x.web_domain
-      Rails.configuration.x.local_domain = 'local.com'
-      Rails.configuration.x.web_domain = 'local.com'
-      example.run
-      Rails.configuration.x.web_domain = web_before
-      Rails.configuration.x.local_domain = local_before
-    end
-
-    before do
-      service = instance_double(ActivityPub::FetchRemoteStatusService)
-      allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service)
-      allow(service).to receive(:call).with('https://unknown-remote.com/users/bar/statuses/1') do
-        Fabricate(:status, uri: 'https://unknown-remote.com/users/bar/statuses/1')
-      end
-    end
-
-    describe 'when no bookmarks are set' do
-      let(:import) { Import.create(account: account, type: 'bookmarks', data: csv) }
-
-      it 'adds the toots the user has access to to bookmarks' do
-        local_status = Fabricate(:status, account: local_account, uri: 'https://local.com/users/foo/statuses/42', id: 42, local: true)
-        subject.call(import)
-        expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(local_status.id)
-        expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(remote_status.id)
-        expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to_not include(direct_status.id)
-        expect(account.bookmarks.count).to eq 3
-      end
-    end
-  end
-end
diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb
index 4a2f494e0c..c15c23ca30 100644
--- a/spec/services/suspend_account_service_spec.rb
+++ b/spec/services/suspend_account_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-RSpec.describe SuspendAccountService, :inline_jobs do
+RSpec.describe SuspendAccountService do
   shared_examples 'common behavior' do
     subject { described_class.new.call(account) }
 
@@ -11,6 +11,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do
 
     before do
       allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil)
+      allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true)
 
       local_follower.follow!(account)
       list.accounts << account
@@ -23,6 +24,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do
     it 'unmerges from feeds of local followers and changes file mode and preserves suspended flag' do
       expect { subject }
         .to change_file_mode
+        .and enqueue_sidekiq_job(CacheBusterWorker).with(account.media_attachments.first.file.url(:original))
         .and not_change_suspended_flag
       expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower)
       expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list)
@@ -38,17 +40,12 @@ RSpec.describe SuspendAccountService, :inline_jobs do
   end
 
   describe 'suspending a local account' do
-    def match_update_actor_request(req, account)
-      json = JSON.parse(req.body)
+    def match_update_actor_request(json, account)
+      json = JSON.parse(json)
       actor_id = ActivityPub::TagManager.instance.uri_for(account)
       json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended']
     end
 
-    before do
-      stub_request(:post, 'https://alice.com/inbox').to_return(status: 201)
-      stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
-    end
-
     include_examples 'common behavior' do
       let!(:account)         { Fabricate(:account) }
       let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
@@ -61,22 +58,20 @@ RSpec.describe SuspendAccountService, :inline_jobs do
 
       it 'sends an Update actor activity to followers and reporters' do
         subject
-        expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
-        expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
+
+        expect(ActivityPub::DeliveryWorker)
+          .to have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_follower.inbox_url).once
+          .and have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_reporter.inbox_url).once
       end
     end
   end
 
   describe 'suspending a remote account' do
-    def match_reject_follow_request(req, account, followee)
-      json = JSON.parse(req.body)
+    def match_reject_follow_request(json, account, followee)
+      json = JSON.parse(json)
       json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri
     end
 
-    before do
-      stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
-    end
-
     include_examples 'common behavior' do
       let!(:account)        { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
       let!(:local_followee) { Fabricate(:account) }
@@ -88,7 +83,8 @@ RSpec.describe SuspendAccountService, :inline_jobs do
       it 'sends a Reject Follow activity', :aggregate_failures do
         subject
 
-        expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once
+        expect(ActivityPub::DeliveryWorker)
+          .to have_enqueued_sidekiq_job(satisfying { |json| match_reject_follow_request(json, account, local_followee) }, local_followee.id, account.inbox_url).once
       end
     end
   end
diff --git a/spec/support/fasp/provider_request_helper.rb b/spec/support/fasp/provider_request_helper.rb
new file mode 100644
index 0000000000..c5d8ae4919
--- /dev/null
+++ b/spec/support/fasp/provider_request_helper.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module ProviderRequestHelper
+  private
+
+  def stub_provider_request(provider, path: '/', method: :get, response_status: 200, response_body: '')
+    response_body = encode_body(response_body)
+    response_headers = {
+      'content-type' => 'application/json',
+    }.merge(response_authentication_headers(provider, response_status, response_body))
+
+    stub_request(method, provider.url(path))
+      .to_return do |_request|
+        {
+          status: response_status,
+          body: response_body,
+          headers: response_headers,
+        }
+      end
+  end
+
+  def request_authentication_headers(provider, url: root_url, method: :get, body: '')
+    body = encode_body(body)
+    headers = {}
+    headers['content-digest'] = content_digest(body)
+    request = Linzer.new_request(method, url, {}, headers)
+    key = private_key_for(provider)
+    signature = sign(request, key, %w(@method @target-uri content-digest))
+    headers.merge(signature.to_h)
+  end
+
+  def response_authentication_headers(provider, status, body)
+    headers = {}
+    headers['content-digest'] = content_digest(body)
+    response = Linzer.new_response(body, status, headers)
+    key = private_key_for(provider)
+    signature = sign(response, key, %w(@status content-digest))
+    headers.merge(signature.to_h)
+  end
+
+  def private_key_for(provider)
+    @cached_provider_keys ||= {}
+    @cached_provider_keys[provider] ||=
+      begin
+        key = OpenSSL::PKey.generate_key('ed25519')
+        provider.update!(provider_public_key_pem: key.public_to_pem)
+        key
+      end
+
+    {
+      id: provider.id.to_s,
+      private_key: @cached_provider_keys[provider].private_to_pem,
+    }
+  end
+
+  def sign(request_or_response, key, components)
+    message = Linzer::Message.new(request_or_response)
+    linzer_key = Linzer.new_ed25519_key(key[:private_key], key[:id])
+    Linzer.sign(linzer_key, message, components)
+  end
+
+  def encode_body(body)
+    return body if body.nil? || body.is_a?(String)
+
+    encoder = ActionDispatch::RequestEncoder.encoder(:json)
+    encoder.encode_params(body)
+  end
+
+  def content_digest(content)
+    "sha-256=:#{OpenSSL::Digest.base64digest('sha256', content)}:"
+  end
+end
diff --git a/spec/support/feature_flags.rb b/spec/support/feature_flags.rb
new file mode 100644
index 0000000000..186711163b
--- /dev/null
+++ b/spec/support/feature_flags.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+  config.before(:example, :feature) do |example|
+    feature = example.metadata[:feature]
+    allow(Mastodon::Feature).to receive(:"#{feature}_enabled?").and_return(true)
+  end
+end
diff --git a/spec/support/signed_request_helpers.rb b/spec/support/signed_request_helpers.rb
index 8a52179cae..a4423af748 100644
--- a/spec/support/signed_request_helpers.rb
+++ b/spec/support/signed_request_helpers.rb
@@ -18,4 +18,24 @@ module SignedRequestHelpers
 
     super(path, headers: headers, **args)
   end
+
+  def post(path, headers: nil, sign_with: nil, **args)
+    return super(path, headers: headers, **args) if sign_with.nil?
+
+    headers ||= {}
+    headers['Date'] = Time.now.utc.httpdate
+    headers['Host'] = Rails.configuration.x.local_domain
+    headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(args[:params].to_s)}"
+
+    signed_headers = headers.merge('(request-target)' => "post #{path}").slice('(request-target)', 'Host', 'Date', 'Digest')
+
+    key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with)
+    keypair = sign_with.keypair
+    signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
+    signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
+
+    headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
+
+    super(path, headers: headers, **args)
+  end
 end
diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb
index 7d6409c50c..1861e84434 100644
--- a/spec/support/stories/profile_stories.rb
+++ b/spec/support/stories/profile_stories.rb
@@ -23,7 +23,11 @@ module ProfileStories
   def as_a_logged_in_user
     as_a_registered_user
     visit new_user_session_path
+    expect(page)
+      .to have_title(I18n.t('auth.login'))
     fill_in_auth_details(email, password)
+    expect(page)
+      .to have_css('.app-holder')
   end
 
   def as_a_logged_in_admin
diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb
index 18889844f8..44bbc64a59 100644
--- a/spec/support/system_helpers.rb
+++ b/spec/support/system_helpers.rb
@@ -1,9 +1,7 @@
 # frozen_string_literal: true
 
 module SystemHelpers
-  def admin_user
-    Fabricate(:admin_user)
-  end
+  FRONTEND_TRANSLATIONS = JSON.parse Rails.root.join('app', 'javascript', 'mastodon', 'locales', 'en.json').read
 
   def submit_button
     I18n.t('generic.save_changes')
@@ -20,4 +18,8 @@ module SystemHelpers
   def css_id(record)
     "##{dom_id(record)}"
   end
+
+  def frontend_translations(key)
+    FRONTEND_TRANSLATIONS[key]
+  end
 end
diff --git a/spec/system/account_notes_spec.rb b/spec/system/account_notes_spec.rb
new file mode 100644
index 0000000000..1d125e1984
--- /dev/null
+++ b/spec/system/account_notes_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Account notes', :inline_jobs, :js, :streaming do
+  include ProfileStories
+
+  let(:email)               { 'test@example.com' }
+  let(:password)            { 'password' }
+  let(:confirmed_at)        { Time.zone.now }
+  let(:finished_onboarding) { true }
+
+  let!(:other_account) { Fabricate(:account) }
+
+  before { as_a_logged_in_user }
+
+  it 'can be written and viewed' do
+    visit_profile(other_account)
+
+    note_text = 'This is a personal note'
+    fill_in frontend_translations('account_note.placeholder'), with: note_text
+
+    # This is a bit awkward since there is no button to save the change
+    # The easiest way is to send ctrl+enter ourselves
+    find_field(class: 'account__header__account-note__content').send_keys [:control, :enter]
+
+    expect(page)
+      .to have_css('.account__header__account-note__content', text: note_text)
+
+    # Navigate back and forth and ensure the comment is still here
+    visit root_url
+    visit_profile(other_account)
+
+    expect(AccountNote.find_by(account: bob.account, target_account: other_account).comment)
+      .to eq note_text
+
+    expect(page)
+      .to have_css('.account__header__account-note__content', text: note_text)
+  end
+
+  def visit_profile(account)
+    visit short_account_path(account)
+
+    expect(page)
+      .to have_css('div.app-holder')
+      .and have_css('form.compose-form')
+  end
+end
diff --git a/spec/system/admin/account_actions_spec.rb b/spec/system/admin/account_actions_spec.rb
new file mode 100644
index 0000000000..787b988a0d
--- /dev/null
+++ b/spec/system/admin/account_actions_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Account Actions' do
+  let(:user) { Fabricate(:admin_user) }
+
+  before { sign_in user }
+
+  describe 'Creating a new account action on an account' do
+    let(:account) { Fabricate(:account) }
+
+    it 'creates the action and redirects to the account page' do
+      visit new_admin_account_action_path(account_id: account.id)
+      expect(page)
+        .to have_title(I18n.t('admin.account_actions.title', acct: account.pretty_acct))
+
+      choose(option: 'silence')
+      expect { submit_form }
+        .to change { account.strikes.count }.by(1)
+      expect(page)
+        .to have_title(account.pretty_acct)
+    end
+
+    def submit_form
+      click_on I18n.t('admin.account_actions.action')
+    end
+  end
+end
diff --git a/spec/controllers/admin/action_logs_controller_spec.rb b/spec/system/admin/action_logs_spec.rb
similarity index 65%
rename from spec/controllers/admin/action_logs_controller_spec.rb
rename to spec/system/admin/action_logs_spec.rb
index d3108e8055..b6a6996f91 100644
--- a/spec/controllers/admin/action_logs_controller_spec.rb
+++ b/spec/system/admin/action_logs_spec.rb
@@ -2,29 +2,33 @@
 
 require 'rails_helper'
 
-RSpec.describe Admin::ActionLogsController do
-  render_views
-
+RSpec.describe 'Admin Action Logs' do
   # Action logs typically cause issues when their targets are not in the database
   let!(:account) { Fabricate(:account) }
 
   before do
-    orphaned_log_types.map do |type|
-      Fabricate(:action_log, account: account, action: 'destroy', target_type: type, target_id: 1312)
-    end
+    populate_action_logs
+    sign_in Fabricate(:admin_user)
   end
 
-  describe 'GET #index' do
-    it 'returns 200' do
-      sign_in Fabricate(:admin_user)
-      get :index, params: { page: 1 }
+  describe 'Viewing action logs' do
+    it 'shows page with action logs listed' do
+      visit admin_action_logs_path
 
-      expect(response).to have_http_status(200)
+      expect(page)
+        .to have_title(I18n.t('admin.action_logs.title'))
+        .and have_css('.log-entry')
     end
   end
 
   private
 
+  def populate_action_logs
+    orphaned_log_types.map do |type|
+      Fabricate(:action_log, account: account, action: 'destroy', target_type: type, target_id: 1312)
+    end
+  end
+
   def orphaned_log_types
     %w(
       Account
diff --git a/spec/system/admin/announcements/distributions_spec.rb b/spec/system/admin/announcements/distributions_spec.rb
new file mode 100644
index 0000000000..a503d466b0
--- /dev/null
+++ b/spec/system/admin/announcements/distributions_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Announcement Mail Distributions' do
+  let(:user) { Fabricate(:admin_user) }
+  let(:announcement) { Fabricate(:announcement, notification_sent_at: nil) }
+
+  before { sign_in(user) }
+
+  describe 'Sending an announcement notification', :inline_jobs do
+    it 'marks the announcement as notified and sends the email' do
+      visit admin_announcement_preview_path(announcement)
+      expect(page)
+        .to have_title(I18n.t('admin.announcements.preview.title'))
+
+      emails = capture_emails do
+        expect { click_on I18n.t('admin.terms_of_service.preview.send_to_all', count: 1, display_count: 1) }
+          .to(change { announcement.reload.notification_sent_at })
+      end
+      expect(emails.first)
+        .to be_present
+        .and(deliver_to(user.email))
+      expect(page)
+        .to have_title(I18n.t('admin.announcements.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/announcements/previews_spec.rb b/spec/system/admin/announcements/previews_spec.rb
new file mode 100644
index 0000000000..0c9a931cb2
--- /dev/null
+++ b/spec/system/admin/announcements/previews_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Announcements Mail Previews' do
+  let(:admin_user) { Fabricate(:admin_user) }
+  let(:announcement) { Fabricate(:announcement, notification_sent_at: nil) }
+
+  before { sign_in(admin_user) }
+
+  describe 'Viewing Announcements Mail previews' do
+    it 'shows the Announcement Mail preview page' do
+      visit admin_announcement_preview_path(announcement)
+
+      expect(page)
+        .to have_title(I18n.t('admin.announcements.preview.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/announcements/tests_spec.rb b/spec/system/admin/announcements/tests_spec.rb
new file mode 100644
index 0000000000..00767c06e8
--- /dev/null
+++ b/spec/system/admin/announcements/tests_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin TermsOfService Tests' do
+  let(:user) { Fabricate(:admin_user) }
+  let(:announcement) { Fabricate(:announcement, notification_sent_at: nil) }
+
+  before { sign_in(user) }
+
+  describe 'Sending test Announcement email', :inline_jobs do
+    it 'generates the test email' do
+      visit admin_announcement_preview_path(announcement)
+      expect(page)
+        .to have_title(I18n.t('admin.announcements.preview.title'))
+
+      emails = capture_emails { click_on I18n.t('admin.terms_of_service.preview.send_preview', email: user.email) }
+      expect(emails.first)
+        .to be_present
+        .and(deliver_to(user.email))
+      expect(page)
+        .to have_title(I18n.t('admin.announcements.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/announcements_spec.rb b/spec/system/admin/announcements_spec.rb
index 65768eb18b..c1cb00cd84 100644
--- a/spec/system/admin/announcements_spec.rb
+++ b/spec/system/admin/announcements_spec.rb
@@ -117,7 +117,7 @@ RSpec.describe 'Admin::Announcements' do
   end
 
   def text_label
-    I18n.t('simple_form.labels.announcement.text')
+    form_label('announcement.text')
   end
 
   def admin_user
diff --git a/spec/system/admin/change_emails_spec.rb b/spec/system/admin/change_emails_spec.rb
new file mode 100644
index 0000000000..6592ddff7c
--- /dev/null
+++ b/spec/system/admin/change_emails_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Change Emails' do
+  let(:admin) { Fabricate(:admin_user) }
+
+  before { sign_in admin }
+
+  describe 'Changing the email address for a user', :inline_jobs do
+    let(:user) { Fabricate :user }
+
+    it 'updates user details and sends email' do
+      visit admin_account_change_email_path(user.account.id)
+      expect(page)
+        .to have_title(I18n.t('admin.accounts.change_email.title', username: user.account.username))
+
+      fill_in 'user_unconfirmed_email', with: 'test@host.example'
+      emails = capture_emails { process_change_email }
+      expect(emails.first)
+        .to be_present
+        .and(deliver_to('test@host.example'))
+        .and(have_subject(/Confirm email/))
+      expect(page)
+        .to have_title(user.account.pretty_acct)
+    end
+
+    def process_change_email
+      expect { click_on I18n.t('admin.accounts.change_email.submit') }
+        .to not_change { user.reload.email }
+        .and(change { user.reload.unconfirmed_email }.to('test@host.example'))
+        .and(change { user.reload.confirmation_token }.from(nil).to(be_present))
+    end
+  end
+end
diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/system/admin/dashboard_spec.rb
similarity index 50%
rename from spec/controllers/admin/dashboard_controller_spec.rb
rename to spec/system/admin/dashboard_spec.rb
index 5a1ea848cc..06d31cde44 100644
--- a/spec/controllers/admin/dashboard_controller_spec.rb
+++ b/spec/system/admin/dashboard_spec.rb
@@ -2,10 +2,8 @@
 
 require 'rails_helper'
 
-RSpec.describe Admin::DashboardController do
-  render_views
-
-  describe 'GET #index' do
+RSpec.describe 'Admin Dashboard' do
+  describe 'Viewing the dashboard page' do
     let(:user) { Fabricate(:owner_user) }
 
     before do
@@ -14,14 +12,12 @@ RSpec.describe Admin::DashboardController do
       sign_in(user)
     end
 
-    it 'returns http success and body with system check messages' do
-      get :index
+    it 'returns page with system check messages' do
+      visit admin_dashboard_path
 
-      expect(response)
-        .to have_http_status(200)
-        .and have_attributes(
-          body: include(I18n.t('admin.system_checks.software_version_patch_check.message_html'))
-        )
+      expect(page)
+        .to have_title(I18n.t('admin.dashboard.title'))
+        .and have_content(I18n.t('admin.system_checks.software_version_patch_check.message_html'))
     end
 
     private
diff --git a/spec/system/admin/fasp/debug/callbacks_spec.rb b/spec/system/admin/fasp/debug/callbacks_spec.rb
new file mode 100644
index 0000000000..0e47aac677
--- /dev/null
+++ b/spec/system/admin/fasp/debug/callbacks_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Debug FASP Callback Management', feature: :fasp do
+  before { sign_in Fabricate(:admin_user) }
+
+  describe 'Viewing and deleting callbacks' do
+    let(:provider) { Fabricate(:fasp_provider, name: 'debug prov') }
+
+    before do
+      Fabricate(:fasp_debug_callback, fasp_provider: provider, request_body: 'called back')
+    end
+
+    it 'displays callbacks and allows to delete them' do
+      visit admin_fasp_debug_callbacks_path
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.debug.callbacks.title'))
+      expect(page).to have_css('td', text: 'debug prov')
+      expect(page).to have_css('code', text: 'called back')
+
+      expect do
+        click_on I18n.t('admin.fasp.debug.callbacks.delete')
+
+        expect(page).to have_css('h2', text: I18n.t('admin.fasp.debug.callbacks.title'))
+      end.to change(Fasp::DebugCallback, :count).by(-1)
+    end
+  end
+end
diff --git a/spec/system/admin/fasp/debug_calls_spec.rb b/spec/system/admin/fasp/debug_calls_spec.rb
new file mode 100644
index 0000000000..d2f6a3a08b
--- /dev/null
+++ b/spec/system/admin/fasp/debug_calls_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'FASP Debug Calls', feature: :fasp do
+  include ProviderRequestHelper
+
+  before { sign_in Fabricate(:admin_user) }
+
+  describe 'Triggering a FASP debug call' do
+    let!(:provider) { Fabricate(:debug_fasp) }
+    let!(:debug_call) do
+      stub_provider_request(provider,
+                            method: :post,
+                            path: '/debug/v0/callback/logs',
+                            response_status: 201)
+    end
+
+    it 'makes a debug call to the provider' do
+      visit admin_fasp_providers_path
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title'))
+      expect(page).to have_css('td', text: provider.name)
+
+      within 'table#providers' do
+        click_on I18n.t('admin.fasp.providers.callback')
+      end
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title'))
+      expect(debug_call).to have_been_requested
+    end
+  end
+end
diff --git a/spec/system/admin/fasp/providers_spec.rb b/spec/system/admin/fasp/providers_spec.rb
new file mode 100644
index 0000000000..03837ad5d9
--- /dev/null
+++ b/spec/system/admin/fasp/providers_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'FASP Management', feature: :fasp do
+  include ProviderRequestHelper
+
+  before { sign_in Fabricate(:admin_user) }
+
+  describe 'Managing capabilities' do
+    let!(:provider) { Fabricate(:confirmed_fasp) }
+    let!(:enable_call) do
+      stub_provider_request(provider,
+                            method: :post,
+                            path: '/capabilities/callback/0/activation')
+    end
+    let!(:disable_call) do
+      stub_provider_request(provider,
+                            method: :delete,
+                            path: '/capabilities/callback/0/activation')
+    end
+
+    before do
+      # We currently err on the side of caution and prefer to send
+      # a "disable capability" call too often over risking to miss
+      # one. So the following call _can_ happen here, and if it does
+      # that is fine, but it has no bearing on the behavior that is
+      # being tested.
+      stub_provider_request(provider,
+                            method: :delete,
+                            path: '/capabilities/data_sharing/0/activation')
+    end
+
+    it 'allows enabling and disabling of capabilities' do
+      visit admin_fasp_providers_path
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title'))
+      expect(page).to have_css('td', text: provider.name)
+
+      click_on I18n.t('admin.fasp.providers.edit')
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.edit'))
+
+      check 'callback'
+
+      click_on I18n.t('admin.fasp.providers.save')
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title'))
+      expect(provider.reload).to be_capability_enabled('callback')
+      expect(enable_call).to have_been_requested
+
+      click_on I18n.t('admin.fasp.providers.edit')
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.edit'))
+
+      uncheck 'callback'
+
+      click_on I18n.t('admin.fasp.providers.save')
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title'))
+      expect(provider.reload).to_not be_capability_enabled('callback')
+      expect(disable_call).to have_been_requested
+    end
+  end
+
+  describe 'Removing a provider' do
+    let!(:provider) { Fabricate(:fasp_provider) }
+
+    it 'allows to completely remove a provider' do
+      visit admin_fasp_providers_path
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title'))
+      expect(page).to have_css('td', text: provider.name)
+
+      click_on I18n.t('admin.fasp.providers.delete')
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title'))
+      expect(page).to have_no_css('td', text: provider.name)
+    end
+  end
+end
diff --git a/spec/system/admin/fasp/registrations_spec.rb b/spec/system/admin/fasp/registrations_spec.rb
new file mode 100644
index 0000000000..3da6f01915
--- /dev/null
+++ b/spec/system/admin/fasp/registrations_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'FASP registration', feature: :fasp do
+  include ProviderRequestHelper
+
+  before { sign_in Fabricate(:admin_user) }
+
+  describe 'Confirming an unconfirmed FASP' do
+    let(:provider) { Fabricate(:fasp_provider, confirmed: false) }
+
+    before do
+      stub_provider_request(provider,
+                            path: '/provider_info',
+                            response_body: {
+                              capabilities: [
+                                { id: 'debug', version: '0.1' },
+                              ],
+                              contactEmail: 'newcontact@example.com',
+                              fediverseAccount: '@newfedi@social.example.com',
+                              privacyPolicy: 'https::///example.com/privacy',
+                              signInUrl: 'https://myprov.example.com/sign_in',
+                            })
+    end
+
+    it 'displays key fingerprint and updates the provider on confirmation' do
+      visit new_admin_fasp_provider_registration_path(provider)
+
+      expect(page).to have_css('code', text: provider.provider_public_key_fingerprint)
+
+      click_on I18n.t('admin.fasp.providers.registrations.confirm')
+
+      expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.edit'))
+
+      expect(provider.reload).to be_confirmed
+    end
+  end
+end
diff --git a/spec/system/admin/invites_spec.rb b/spec/system/admin/invites_spec.rb
index cc1e4bae18..53300c1ad0 100644
--- a/spec/system/admin/invites_spec.rb
+++ b/spec/system/admin/invites_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe 'Admin Invites' do
     end
 
     def max_use_field
-      I18n.t('simple_form.labels.defaults.max_uses')
+      form_label('defaults.max_uses')
     end
   end
 end
diff --git a/spec/system/admin/relationships_spec.rb b/spec/system/admin/relationships_spec.rb
new file mode 100644
index 0000000000..ba90fe156e
--- /dev/null
+++ b/spec/system/admin/relationships_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Relationships' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
+  describe 'Viewing account relationships page' do
+    let(:account) { Fabricate(:account) }
+
+    it 'shows page with relationships for account' do
+      visit admin_account_relationships_path(account.id)
+
+      expect(page)
+        .to have_title(I18n.t('admin.relationships.title', acct: account.pretty_acct))
+    end
+  end
+end
diff --git a/spec/system/admin/reports_spec.rb b/spec/system/admin/reports_spec.rb
new file mode 100644
index 0000000000..90845a02f7
--- /dev/null
+++ b/spec/system/admin/reports_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Reports' do
+  let(:user) { Fabricate(:admin_user) }
+
+  before { sign_in(user) }
+
+  describe 'Viewing existing reports' do
+    let!(:unresolved_report) { Fabricate(:report, action_taken_at: nil, comment: 'First report') }
+    let!(:resolved_report) { Fabricate(:report, action_taken_at: Time.now.utc, comment: 'Second report') }
+    let!(:report_note) { Fabricate :report_note, report: resolved_report, content: 'Note about resolved report' }
+
+    it 'Shows basic report details' do
+      visit admin_reports_path
+
+      expect(page)
+        .to have_content(unresolved_report.comment)
+        .and have_no_content(resolved_report.comment)
+
+      click_on I18n.t('admin.reports.resolved')
+      expect(page)
+        .to have_content(resolved_report.comment)
+        .and have_no_content(unresolved_report.comment)
+
+      click_on resolved_report.comment
+      expect(page)
+        .to have_title(I18n.t('admin.reports.report', id: resolved_report.id))
+        .and have_content(resolved_report.comment)
+        .and have_content(report_note.content)
+    end
+  end
+
+  describe 'Resolving reports' do
+    let!(:report) { Fabricate :report }
+
+    it 'resolves an open report' do
+      visit admin_report_path(report)
+      within '.content__heading__actions' do
+        click_on I18n.t('admin.reports.mark_as_resolved')
+      end
+
+      expect(page)
+        .to have_title(I18n.t('admin.reports.title'))
+        .and have_content(I18n.t('admin.reports.resolved_msg'))
+
+      report.reload
+      expect(report.action_taken_by_account)
+        .to eq user.account
+      expect(report)
+        .to be_action_taken
+      expect(last_action_log.target)
+        .to eq(report)
+    end
+  end
+
+  describe 'Reopening reports' do
+    let!(:report) { Fabricate :report, action_taken_at: 3.days.ago }
+
+    it 'reopens a resolved report' do
+      visit admin_report_path(report)
+      within '.content__heading__actions' do
+        click_on I18n.t('admin.reports.mark_as_unresolved')
+      end
+
+      expect(page)
+        .to have_title(I18n.t('admin.reports.report', id: report.id))
+
+      report.reload
+      expect(report.action_taken_by_account)
+        .to be_nil
+      expect(report)
+        .to_not be_action_taken
+      expect(last_action_log.target)
+        .to eq(report)
+    end
+  end
+
+  describe 'Assigning reports' do
+    let!(:report) { Fabricate :report }
+
+    it 'assigns report to user and then unassigns' do
+      visit admin_report_path(report)
+
+      click_on I18n.t('admin.reports.assign_to_self')
+
+      expect(page)
+        .to have_title(I18n.t('admin.reports.report', id: report.id))
+      report.reload
+      expect(report.assigned_account)
+        .to eq user.account
+      expect(last_action_log.target)
+        .to eq(report)
+
+      click_on I18n.t('admin.reports.unassign')
+      expect(page)
+        .to have_title(I18n.t('admin.reports.report', id: report.id))
+      report.reload
+      expect(report.assigned_account)
+        .to be_nil
+      expect(last_action_log.target)
+        .to eq(report)
+    end
+  end
+
+  private
+
+  def last_action_log
+    Admin::ActionLog.last
+  end
+end
diff --git a/spec/system/admin/roles_spec.rb b/spec/system/admin/roles_spec.rb
new file mode 100644
index 0000000000..2a82d80b71
--- /dev/null
+++ b/spec/system/admin/roles_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin::Roles' do
+  context 'when user has administrator permissions' do
+    let(:user_role) { Fabricate(:user_role, permissions: UserRole::FLAGS[:administrator], position: 10) }
+
+    before { sign_in Fabricate(:user, role: user_role) }
+
+    it 'creates new user role' do
+      visit new_admin_role_path
+
+      fill_in 'user_role_name', with: 'Baz'
+      fill_in 'user_role_position', with: '1'
+      check 'user_role_permissions_as_keys_manage_reports'
+      check 'user_role_permissions_as_keys_manage_roles'
+
+      expect { click_on I18n.t('admin.roles.add_new') }
+        .to change(UserRole, :count)
+      expect(page)
+        .to have_title(I18n.t('admin.roles.title'))
+    end
+  end
+
+  context 'when user has permissions to manage roles' do
+    let(:user_role) { Fabricate(:user_role, permissions: UserRole::FLAGS[:manage_roles], position: 10) }
+
+    before { sign_in Fabricate(:user, role: user_role) }
+
+    it 'Creates user roles' do
+      visit admin_roles_path
+      expect(page)
+        .to have_title(I18n.t('admin.roles.title'))
+
+      click_on I18n.t('admin.roles.add_new')
+      expect(page)
+        .to have_title(I18n.t('admin.roles.add_new'))
+
+      # Position too high
+      fill_in 'user_role_name', with: 'Baz'
+      fill_in 'user_role_position', with: '100'
+      expect { click_on I18n.t('admin.roles.add_new') }
+        .to_not change(UserRole, :count)
+      expect(page)
+        .to have_content(I18n.t('activerecord.errors.models.user_role.attributes.position.elevated'))
+
+      # Valid submission
+      fill_in 'user_role_name', with: 'Baz'
+      fill_in 'user_role_position', with: '5' # Lower than user
+      check 'user_role_permissions_as_keys_manage_roles' # User has permission
+      expect { click_on I18n.t('admin.roles.add_new') }
+        .to change(UserRole, :count)
+      expect(page)
+        .to have_title(I18n.t('admin.roles.title'))
+    end
+
+    it 'Manages existing user roles' do
+      role = Fabricate :user_role, name: 'Baz'
+
+      visit edit_admin_role_path(role)
+      expect(page)
+        .to have_title(I18n.t('admin.roles.edit', name: 'Baz'))
+
+      # Update role attribute
+      fill_in 'user_role_position', with: '5' # Lower than user
+      expect { click_on submit_button }
+        .to(change { role.reload.position })
+
+      # Destroy the role
+      visit edit_admin_role_path(role)
+      expect { click_on I18n.t('admin.roles.delete') }
+        .to change(UserRole, :count).by(-1)
+      expect(page)
+        .to have_title(I18n.t('admin.roles.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/rules_spec.rb b/spec/system/admin/rules_spec.rb
index a7eb3a0fce..95391ba33d 100644
--- a/spec/system/admin/rules_spec.rb
+++ b/spec/system/admin/rules_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe 'Admin Rules' do
       end
 
       def submit_form
-        click_on I18n.t('generic.save_changes')
+        click_on(submit_button)
       end
     end
 
diff --git a/spec/system/admin/settings/about_spec.rb b/spec/system/admin/settings/about_spec.rb
index c7405a8d5a..93ee3f6864 100644
--- a/spec/system/admin/settings/about_spec.rb
+++ b/spec/system/admin/settings/about_spec.rb
@@ -3,9 +3,14 @@
 require 'rails_helper'
 
 RSpec.describe 'Admin::Settings::About' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
   it 'Saves changes to about settings' do
-    sign_in admin_user
     visit admin_settings_about_path
+    expect(page)
+      .to have_title(I18n.t('admin.settings.about.title'))
 
     fill_in extended_description_field,
             with: 'new site description'
diff --git a/spec/system/admin/settings/appearance_spec.rb b/spec/system/admin/settings/appearance_spec.rb
index 56af58c812..2f6e67979e 100644
--- a/spec/system/admin/settings/appearance_spec.rb
+++ b/spec/system/admin/settings/appearance_spec.rb
@@ -3,9 +3,14 @@
 require 'rails_helper'
 
 RSpec.describe 'Admin::Settings::Appearance' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
   it 'Saves changes to appearance settings' do
-    sign_in admin_user
     visit admin_settings_appearance_path
+    expect(page)
+      .to have_title(I18n.t('admin.settings.appearance.title'))
 
     fill_in custom_css_field,
             with: 'html { display: inline; }'
diff --git a/spec/system/admin/settings/branding_spec.rb b/spec/system/admin/settings/branding_spec.rb
index 5cd9319ce0..78364669e9 100644
--- a/spec/system/admin/settings/branding_spec.rb
+++ b/spec/system/admin/settings/branding_spec.rb
@@ -3,9 +3,14 @@
 require 'rails_helper'
 
 RSpec.describe 'Admin::Settings::Branding' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
   it 'Saves changes to branding settings' do
-    sign_in admin_user
     visit admin_settings_branding_path
+    expect(page)
+      .to have_title(I18n.t('admin.settings.branding.title'))
 
     fill_in short_description_field,
             with: 'new key value'
diff --git a/spec/system/admin/settings/content_retention_spec.rb b/spec/system/admin/settings/content_retention_spec.rb
index f788f8eea0..b813c4fa5b 100644
--- a/spec/system/admin/settings/content_retention_spec.rb
+++ b/spec/system/admin/settings/content_retention_spec.rb
@@ -3,9 +3,14 @@
 require 'rails_helper'
 
 RSpec.describe 'Admin::Settings::ContentRetention' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
   it 'Saves changes to content retention settings' do
-    sign_in admin_user
     visit admin_settings_content_retention_path
+    expect(page)
+      .to have_title(I18n.t('admin.settings.content_retention.title'))
 
     fill_in media_cache_retention_period_field,
             with: '2'
diff --git a/spec/system/admin/settings/discovery_spec.rb b/spec/system/admin/settings/discovery_spec.rb
index f000d18370..f6909da9be 100644
--- a/spec/system/admin/settings/discovery_spec.rb
+++ b/spec/system/admin/settings/discovery_spec.rb
@@ -3,9 +3,14 @@
 require 'rails_helper'
 
 RSpec.describe 'Admin::Settings::Discovery' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
   it 'Saves changes to discovery settings' do
-    sign_in admin_user
     visit admin_settings_discovery_path
+    expect(page)
+      .to have_title(I18n.t('admin.settings.discovery.title'))
 
     check trends_box
 
diff --git a/spec/system/admin/settings/registrations_spec.rb b/spec/system/admin/settings/registrations_spec.rb
index d026b07c85..9b8bef4172 100644
--- a/spec/system/admin/settings/registrations_spec.rb
+++ b/spec/system/admin/settings/registrations_spec.rb
@@ -3,9 +3,14 @@
 require 'rails_helper'
 
 RSpec.describe 'Admin::Settings::Registrations' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
   it 'Saves changes to registrations settings' do
-    sign_in admin_user
     visit admin_settings_registrations_path
+    expect(page)
+      .to have_title(I18n.t('admin.settings.registrations.title'))
 
     select open_mode_option,
            from: registrations_mode_field
diff --git a/spec/system/admin/site_uploads_spec.rb b/spec/system/admin/site_uploads_spec.rb
new file mode 100644
index 0000000000..5cbd8d275c
--- /dev/null
+++ b/spec/system/admin/site_uploads_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin SiteUploads' do
+  let(:user) { Fabricate(:admin_user) }
+
+  before { sign_in(user) }
+
+  describe 'Removing a site upload' do
+    let!(:site_upload) { Fabricate(:site_upload, var: 'thumbnail') }
+
+    it 'removes the upload and redirects' do
+      visit admin_settings_branding_path
+      expect(page)
+        .to have_title(I18n.t('admin.settings.branding.title'))
+
+      expect { click_on I18n.t('admin.site_uploads.delete') }
+        .to change(SiteUpload, :count).by(-1)
+      expect { site_upload.reload }
+        .to raise_error(ActiveRecord::RecordNotFound)
+      expect(page)
+        .to have_content(I18n.t('admin.site_uploads.destroyed_msg'))
+        .and have_title(I18n.t('admin.settings.branding.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/software_updates_spec.rb b/spec/system/admin/software_updates_spec.rb
index f49c5a3c87..e62b6a8cfe 100644
--- a/spec/system/admin/software_updates_spec.rb
+++ b/spec/system/admin/software_updates_spec.rb
@@ -5,6 +5,7 @@ require 'rails_helper'
 RSpec.describe 'finding software updates through the admin interface' do
   before do
     Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true, release_notes: 'https://github.com/mastodon/mastodon/releases/v99')
+    Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: true, release_notes: 'https://github.com/mastodon/mastodon/releases/v3.5.0')
 
     sign_in Fabricate(:owner_user), scope: :user
   end
@@ -16,6 +17,7 @@ RSpec.describe 'finding software updates through the admin interface' do
     expect(page).to have_title(I18n.t('admin.software_updates.title'))
 
     expect(page).to have_content('99.99.99')
+                .and have_no_content('3.5.0')
 
     click_on I18n.t('admin.software_updates.release_notes')
     expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true)
diff --git a/spec/system/admin/tags_spec.rb b/spec/system/admin/tags_spec.rb
index 91227f0ca7..654fac3340 100644
--- a/spec/system/admin/tags_spec.rb
+++ b/spec/system/admin/tags_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe 'Admin Tags' do
     end
 
     def display_name_field
-      I18n.t('simple_form.labels.defaults.display_name')
+      form_label('defaults.display_name')
     end
 
     def match_error_text
diff --git a/spec/system/admin/terms_of_service/distributions_spec.rb b/spec/system/admin/terms_of_service/distributions_spec.rb
new file mode 100644
index 0000000000..ba525d09c0
--- /dev/null
+++ b/spec/system/admin/terms_of_service/distributions_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin TermsOfService Distributions' do
+  let(:user) { Fabricate(:admin_user) }
+  let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
+
+  before { sign_in(user) }
+
+  describe 'Sending a TOS change notification', :inline_jobs do
+    it 'marks the TOS as notified and sends the email' do
+      visit admin_terms_of_service_preview_path(terms_of_service)
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.preview.title'))
+
+      emails = capture_emails do
+        expect { click_on I18n.t('admin.terms_of_service.preview.send_to_all', count: 1, display_count: 1) }
+          .to(change { terms_of_service.reload.notification_sent_at })
+      end
+      expect(emails.first)
+        .to be_present
+        .and(deliver_to(user.email))
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/terms_of_service/drafts_spec.rb b/spec/system/admin/terms_of_service/drafts_spec.rb
new file mode 100644
index 0000000000..cf4c10ce00
--- /dev/null
+++ b/spec/system/admin/terms_of_service/drafts_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin TermsOfService Drafts' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
+  describe 'Managing TOS drafts' do
+    let!(:terms) { Fabricate :terms_of_service, published_at: nil }
+
+    it 'saves and publishes TOS drafts' do
+      visit admin_terms_of_service_draft_path
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.title'))
+
+      # Invalid submission
+      expect { click_on I18n.t('admin.terms_of_service.save_draft') }
+        .to_not(change { terms.reload.published_at })
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.title'))
+
+      # Valid submission with draft button
+      fill_in 'terms_of_service_text', with: 'new'
+      expect { click_on I18n.t('admin.terms_of_service.save_draft') }
+        .to not_change { terms.reload.published_at }.from(nil)
+        .and not_change(Admin::ActionLog, :count)
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.title'))
+
+      # Valid with publish button
+      fill_in 'terms_of_service_text', with: 'newer'
+      expect { click_on I18n.t('admin.terms_of_service.publish') }
+        .to change { terms.reload.published_at }.from(nil)
+        .and change(Admin::ActionLog, :count)
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/terms_of_service/generates_spec.rb b/spec/system/admin/terms_of_service/generates_spec.rb
new file mode 100644
index 0000000000..431084314c
--- /dev/null
+++ b/spec/system/admin/terms_of_service/generates_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin TermsOfService Generates' do
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
+  describe 'Generating a TOS policy' do
+    it 'saves a new TOS from values' do
+      visit admin_terms_of_service_generate_path
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.generates.title'))
+
+      # Invalid form submission
+      fill_in 'terms_of_service_generator_admin_email', with: 'what the'
+      expect { submit_form }
+        .to_not change(TermsOfService, :count)
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.generates.title'))
+
+      # Valid submission
+      fill_in 'terms_of_service_generator_admin_email', with: 'test@host.example'
+      fill_in 'terms_of_service_generator_arbitration_address', with: '123 Main Street'
+      fill_in 'terms_of_service_generator_arbitration_website', with: 'https://host.example'
+      fill_in 'terms_of_service_generator_dmca_address', with: '123 DMCA Ave'
+      fill_in 'terms_of_service_generator_dmca_email', with: 'dmca@host.example'
+      fill_in 'terms_of_service_generator_domain', with: 'host.example'
+      fill_in 'terms_of_service_generator_jurisdiction', with: 'Europe'
+      fill_in 'terms_of_service_generator_choice_of_law', with: 'New York'
+      fill_in 'terms_of_service_generator_min_age', with: '16'
+
+      expect { submit_form }
+        .to change(TermsOfService, :count).by(1)
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.title'))
+    end
+
+    def submit_form
+      click_on I18n.t('admin.terms_of_service.generates.action')
+    end
+  end
+end
diff --git a/spec/system/admin/terms_of_service/previews_spec.rb b/spec/system/admin/terms_of_service/previews_spec.rb
new file mode 100644
index 0000000000..df72e601dc
--- /dev/null
+++ b/spec/system/admin/terms_of_service/previews_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin TermsOfService Previews' do
+  let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
+  let(:admin_user) { Fabricate(:admin_user) }
+
+  before { sign_in(admin_user) }
+
+  describe 'Viewing TOS previews' do
+    it 'shows the TOS preview page' do
+      visit admin_terms_of_service_preview_path(terms_of_service)
+
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.preview.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/terms_of_service/tests_spec.rb b/spec/system/admin/terms_of_service/tests_spec.rb
new file mode 100644
index 0000000000..3fc7d4e75d
--- /dev/null
+++ b/spec/system/admin/terms_of_service/tests_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin TermsOfService Tests' do
+  let(:user) { Fabricate(:admin_user) }
+  let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) }
+
+  before { sign_in(user) }
+
+  describe 'Sending test TOS email', :inline_jobs do
+    it 'generates the test email' do
+      visit admin_terms_of_service_preview_path(terms_of_service)
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.preview.title'))
+
+      emails = capture_emails { click_on I18n.t('admin.terms_of_service.preview.send_preview', email: user.email) }
+      expect(emails.first)
+        .to be_present
+        .and(deliver_to(user.email))
+      expect(page)
+        .to have_title(I18n.t('admin.terms_of_service.preview.title'))
+    end
+  end
+end
diff --git a/spec/system/admin/trends/links/preview_card_providers_spec.rb b/spec/system/admin/trends/links/preview_card_providers_spec.rb
index 159a5b720a..4636ca86b2 100644
--- a/spec/system/admin/trends/links/preview_card_providers_spec.rb
+++ b/spec/system/admin/trends/links/preview_card_providers_spec.rb
@@ -11,6 +11,8 @@ RSpec.describe 'Admin::Trends::Links::PreviewCardProviders' do
     context 'without selecting any records' do
       it 'displays a notice about selection' do
         visit admin_trends_links_preview_card_providers_path
+        expect(page)
+          .to have_title(I18n.t('admin.trends.preview_card_providers.title'))
 
         click_on button_for_allow
 
diff --git a/spec/system/admin/trends/links_spec.rb b/spec/system/admin/trends/links_spec.rb
index 879bbe8ad9..6140ea8154 100644
--- a/spec/system/admin/trends/links_spec.rb
+++ b/spec/system/admin/trends/links_spec.rb
@@ -11,6 +11,8 @@ RSpec.describe 'Admin::Trends::Links' do
     context 'without selecting any records' do
       it 'displays a notice about selection' do
         visit admin_trends_links_path
+        expect(page)
+          .to have_title(I18n.t('admin.trends.links.title'))
 
         click_on button_for_allow
 
diff --git a/spec/system/admin/trends/statuses_spec.rb b/spec/system/admin/trends/statuses_spec.rb
index be081df989..6e1aa17b7d 100644
--- a/spec/system/admin/trends/statuses_spec.rb
+++ b/spec/system/admin/trends/statuses_spec.rb
@@ -11,6 +11,8 @@ RSpec.describe 'Admin::Trends::Statuses' do
     context 'without selecting any records' do
       it 'displays a notice about selection' do
         visit admin_trends_statuses_path
+        expect(page)
+          .to have_title(I18n.t('admin.trends.statuses.title'))
 
         click_on button_for_allow
 
diff --git a/spec/system/admin/trends/tags_spec.rb b/spec/system/admin/trends/tags_spec.rb
index a71d9ba8ca..a7f00c0232 100644
--- a/spec/system/admin/trends/tags_spec.rb
+++ b/spec/system/admin/trends/tags_spec.rb
@@ -11,6 +11,8 @@ RSpec.describe 'Admin::Trends::Tags' do
     context 'without selecting any records' do
       it 'displays a notice about selection' do
         visit admin_trends_tags_path
+        expect(page)
+          .to have_title(I18n.t('admin.trends.tags.title'))
 
         click_on button_for_allow
 
diff --git a/spec/system/admin/users/roles_spec.rb b/spec/system/admin/users/roles_spec.rb
new file mode 100644
index 0000000000..8b163c4d79
--- /dev/null
+++ b/spec/system/admin/users/roles_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Users Roles' do
+  let(:current_role) { UserRole.create(name: 'Foo', permissions: UserRole::FLAGS[:manage_roles], position: 10) }
+  let(:current_user) { Fabricate(:user, role: current_role) }
+
+  let(:previous_role) { nil }
+  let(:user) { Fabricate(:user, role: previous_role) }
+
+  before do
+    sign_in current_user, scope: :user
+  end
+
+  describe 'Managing user roles' do
+    let!(:too_high_role) { UserRole.create(name: 'TooHigh', permissions: UserRole::FLAGS[:administrator], position: 100) }
+    let!(:usable_role) { UserRole.create(name: 'Usable', permissions: UserRole::FLAGS[:manage_roles], position: 1) }
+
+    it 'selects and updates user roles' do
+      visit admin_user_role_path(user)
+      expect(page)
+        .to have_title I18n.t('admin.accounts.change_role.title', username: user.account.username)
+
+      # Fails to assign not allowed role
+      select too_high_role.name, from: 'user_role_id'
+      expect { click_on submit_button }
+        .to_not(change { user.reload.role_id })
+      expect(page)
+        .to have_title I18n.t('admin.accounts.change_role.title', username: user.account.username)
+
+      # Assigns allowed role
+      select usable_role.name, from: 'user_role_id'
+      expect { click_on submit_button }
+        .to(change { user.reload.role_id }.to(usable_role.id))
+    end
+  end
+end
diff --git a/spec/system/admin/users/two_factor_authentications_spec.rb b/spec/system/admin/users/two_factor_authentications_spec.rb
new file mode 100644
index 0000000000..e09bc437b4
--- /dev/null
+++ b/spec/system/admin/users/two_factor_authentications_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'webauthn/fake_client'
+
+RSpec.describe 'Admin Users TwoFactorAuthentications' do
+  let(:user) { Fabricate(:user) }
+
+  before { sign_in Fabricate(:admin_user) }
+
+  describe 'Disabling 2FA for users' do
+    before { stub_webauthn_credential }
+
+    let(:fake_client) { WebAuthn::FakeClient.new('http://test.host') }
+
+    context 'when user has OTP enabled' do
+      before { user.update(otp_required_for_login: true) }
+
+      it 'disables OTP and redirects to admin account page' do
+        visit admin_account_path(user.account.id)
+
+        expect { disable_two_factor }
+          .to change { user.reload.otp_enabled? }.to(false)
+        expect(page)
+          .to have_title(user.account.pretty_acct)
+      end
+    end
+
+    context 'when user has OTP and WebAuthn enabled' do
+      before { user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id) }
+
+      it 'disables OTP and webauthn and redirects to admin account page' do
+        visit admin_account_path(user.account.id)
+
+        expect { disable_two_factor }
+          .to change { user.reload.otp_enabled? }.to(false)
+          .and(change { user.reload.webauthn_enabled? }.to(false))
+        expect(page)
+          .to have_title(user.account.pretty_acct)
+      end
+    end
+
+    def disable_two_factor
+      click_on I18n.t('admin.accounts.disable_two_factor_authentication')
+    end
+
+    def stub_webauthn_credential
+      public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
+      Fabricate(
+        :webauthn_credential,
+        external_id: public_key_credential.id,
+        nickname: 'Security Key',
+        public_key: public_key_credential.public_key,
+        user_id: user.id
+      )
+    end
+  end
+end
diff --git a/spec/system/admin/warning_presets_spec.rb b/spec/system/admin/warning_presets_spec.rb
index f1ab690981..88c697b743 100644
--- a/spec/system/admin/warning_presets_spec.rb
+++ b/spec/system/admin/warning_presets_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe 'Admin Warning Presets' do
       end
 
       def submit_form
-        click_on I18n.t('generic.save_changes')
+        click_on(submit_button)
       end
     end
 
diff --git a/spec/system/admin/webhooks_spec.rb b/spec/system/admin/webhooks_spec.rb
index cd9eb96da2..709752dc9c 100644
--- a/spec/system/admin/webhooks_spec.rb
+++ b/spec/system/admin/webhooks_spec.rb
@@ -81,7 +81,20 @@ RSpec.describe 'Admin Webhooks' do
       end
 
       def submit_form
-        click_on I18n.t('generic.save_changes')
+        click_on(submit_button)
+      end
+    end
+
+    describe 'Rotate a webhook secret' do
+      let!(:webhook) { Fabricate :webhook, events: [Webhook::EVENTS.first] }
+
+      it 'rotates secret and returns to page' do
+        visit admin_webhook_path(webhook)
+
+        expect { click_on I18n.t('admin.webhooks.rotate_secret') }
+          .to(change { webhook.reload.secret })
+        expect(page)
+          .to have_title(I18n.t('admin.webhooks.title'))
       end
     end
 
diff --git a/spec/system/auth/passwords_spec.rb b/spec/system/auth/passwords_spec.rb
new file mode 100644
index 0000000000..42733b2521
--- /dev/null
+++ b/spec/system/auth/passwords_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Auth Passwords' do
+  let(:user) { Fabricate :user }
+  let!(:session_activation) { Fabricate(:session_activation, user: user) }
+  let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
+  let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
+
+  describe 'Resetting a password', :inline_jobs do
+    let(:new_password) { 'New.Pass.123' }
+
+    before { allow(Devise).to receive(:pam_authentication).and_return(false) } # Avoid the "seamless external" path
+
+    it 'initiates reset, sends link, resets password from form, clears data' do
+      visit new_user_password_path
+      expect(page)
+        .to have_title(I18n.t('auth.reset_password'))
+
+      submit_email_reset
+      expect(page)
+        .to have_title(I18n.t('auth.set_new_password'))
+
+      set_new_password
+      expect(page)
+        .to have_title(I18n.t('auth.login'))
+
+      # Change password
+      expect(User.find(user.id))
+        .to be_present
+        .and be_valid_password(new_password)
+
+      # Deactivate session
+      expect(user_session_count)
+        .to eq(0)
+      expect { session_activation.reload }
+        .to raise_error(ActiveRecord::RecordNotFound)
+
+      # Revoke tokens
+      expect(user_token_count)
+        .to eq(0)
+
+      # Remove push subs
+      expect(push_subs_count)
+        .to eq(0)
+      expect { web_push_subscription.reload }
+        .to raise_error(ActiveRecord::RecordNotFound)
+    end
+
+    def submit_email_reset
+      fill_in 'user_email', with: user.email
+      click_on I18n.t('auth.reset_password')
+      open_last_email
+      visit_in_email(I18n.t('devise.mailer.reset_password_instructions.action'))
+    end
+
+    def set_new_password
+      fill_in 'user_password', with: new_password
+      fill_in 'user_password_confirmation', with: new_password
+      click_on I18n.t('auth.set_new_password')
+    end
+
+    def user_session_count
+      user
+        .session_activations
+        .count
+    end
+
+    def user_token_count
+      Doorkeeper::AccessToken
+        .active_for(user)
+        .count
+    end
+
+    def push_subs_count
+      Web::PushSubscription
+        .where(user: user)
+        .or(Web::PushSubscription.where(access_token: access_token))
+        .count
+    end
+  end
+end
diff --git a/spec/system/disputes/strikes_spec.rb b/spec/system/disputes/strikes_spec.rb
new file mode 100644
index 0000000000..d2b6b08c46
--- /dev/null
+++ b/spec/system/disputes/strikes_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Disputes Strikes' do
+  before { sign_in(current_user) }
+
+  describe 'viewing strike disputes' do
+    let(:current_user) { Fabricate(:user) }
+    let!(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
+
+    it 'shows a list of strikes and details for each' do
+      visit disputes_strikes_path
+      expect(page)
+        .to have_title(I18n.t('settings.strikes'))
+
+      find('.strike-entry').click
+      expect(page)
+        .to have_title(strike_page_title)
+        .and have_content(strike.text)
+    end
+
+    def strike_page_title
+      I18n.t('disputes.strikes.title', action: I18n.t(strike.action, scope: 'disputes.strikes.title_actions'), date: I18n.l(strike.created_at.to_date))
+    end
+  end
+end
diff --git a/spec/system/filters/statuses_spec.rb b/spec/system/filters/statuses_spec.rb
new file mode 100644
index 0000000000..b353bd8674
--- /dev/null
+++ b/spec/system/filters/statuses_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Filters Statuses' do
+  describe 'Viewing statuses under a filter' do
+    let(:filter) { Fabricate(:custom_filter, title: 'good filter') }
+
+    context 'with the filter user signed in' do
+      before { sign_in(filter.account.user) }
+
+      it 'returns a page with the status filters' do
+        visit filter_statuses_path(filter)
+
+        expect(page)
+          .to have_private_cache_control
+          .and have_title(/good filter/)
+      end
+    end
+  end
+end
diff --git a/spec/system/filters_spec.rb b/spec/system/filters_spec.rb
index 0af4e86009..dac04d5b6c 100644
--- a/spec/system/filters_spec.rb
+++ b/spec/system/filters_spec.rb
@@ -108,6 +108,6 @@ RSpec.describe 'Filters' do
   end
 
   def filter_title_field
-    I18n.t('simple_form.labels.defaults.title')
+    form_label('defaults.title')
   end
 end
diff --git a/spec/system/invites_spec.rb b/spec/system/invites_spec.rb
index 5a9b5afa2c..fa3eb5778a 100644
--- a/spec/system/invites_spec.rb
+++ b/spec/system/invites_spec.rb
@@ -74,9 +74,9 @@ RSpec.describe 'Invites' do
 
   def fill_invite_form
     select I18n.t('invites.max_uses', count: 100),
-           from: I18n.t('simple_form.labels.defaults.max_uses')
+           from: form_label('defaults.max_uses')
     select I18n.t("invites.expires_in.#{30.minutes.to_i}"),
-           from: I18n.t('simple_form.labels.defaults.expires_in')
-    check I18n.t('simple_form.labels.defaults.autofollow')
+           from: form_label('defaults.expires_in')
+    check form_label('defaults.autofollow')
   end
 end
diff --git a/spec/system/log_out_spec.rb b/spec/system/log_out_spec.rb
index 2e52254ca0..ebbf5a5772 100644
--- a/spec/system/log_out_spec.rb
+++ b/spec/system/log_out_spec.rb
@@ -17,8 +17,9 @@ RSpec.describe 'Log out' do
         click_on 'Logout'
       end
 
-      expect(page).to have_title(I18n.t('auth.login'))
-      expect(page).to have_current_path('/auth/sign_in')
+      expect(page)
+        .to have_title(I18n.t('auth.login'))
+        .and have_current_path('/auth/sign_in')
     end
   end
 
@@ -28,6 +29,8 @@ RSpec.describe 'Log out' do
       ignore_js_error(/Failed to load resource: the server responded with a status of 422/)
 
       visit root_path
+      expect(page)
+        .to have_css('body', class: 'app-body')
 
       within '.navigation-bar' do
         click_on 'Menu'
@@ -39,8 +42,9 @@ RSpec.describe 'Log out' do
 
       click_on 'Log out'
 
-      expect(page).to have_title(I18n.t('auth.login'))
-      expect(page).to have_current_path('/auth/sign_in')
+      expect(page)
+        .to have_title(I18n.t('auth.login'))
+        .and have_current_path('/auth/sign_in')
     end
   end
 end
diff --git a/spec/system/new_statuses_spec.rb b/spec/system/new_statuses_spec.rb
index 1d38b44a15..87b29004b2 100644
--- a/spec/system/new_statuses_spec.rb
+++ b/spec/system/new_statuses_spec.rb
@@ -5,8 +5,6 @@ require 'rails_helper'
 RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
   include ProfileStories
 
-  subject { page }
-
   let(:email)               { 'test@example.com' }
   let(:password)            { 'password' }
   let(:confirmed_at)        { Time.zone.now }
@@ -14,33 +12,27 @@ RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
 
   before do
     as_a_logged_in_user
-    visit root_path
     page.driver.browser.manage.window.resize_to(1600, 1050)
   end
 
   it 'can be posted' do
-    expect(subject).to have_css('div.app-holder')
-
+    visit_homepage
     status_text = 'This is a new status!'
 
     within('.compose-form') do
-      fill_in "What's on your mind?", with: status_text
+      fill_in frontend_translations('compose_form.placeholder'), with: status_text
       click_on 'Post'
     end
 
-    expect(subject).to have_css('.status__content__text', text: status_text)
+    expect(page)
+      .to have_css('.status__content__text', text: status_text)
   end
 
-  it 'can be posted again' do
-    expect(subject).to have_css('div.app-holder')
+  def visit_homepage
+    visit root_path
 
-    status_text = 'This is a second status!'
-
-    within('.compose-form') do
-      fill_in "What's on your mind?", with: status_text
-      click_on 'Post'
-    end
-
-    expect(subject).to have_css('.status__content__text', text: status_text)
+    expect(page)
+      .to have_css('div.app-holder')
+      .and have_css('form.compose-form')
   end
 end
diff --git a/spec/system/report_interface_spec.rb b/spec/system/report_interface_spec.rb
index 6a90aa5bc6..3df6b3714b 100644
--- a/spec/system/report_interface_spec.rb
+++ b/spec/system/report_interface_spec.rb
@@ -40,5 +40,7 @@ RSpec.describe 'report interface', :attachment_processing, :js, :streaming do
     within '.report-actions' do
       click_on I18n.t('admin.reports.mark_as_resolved')
     end
+    expect(page)
+      .to have_content(I18n.t('admin.reports.resolved_msg'))
   end
 end
diff --git a/spec/system/settings/applications_spec.rb b/spec/system/settings/applications_spec.rb
index ee43da3d5d..5a8c97dd1e 100644
--- a/spec/system/settings/applications_spec.rb
+++ b/spec/system/settings/applications_spec.rb
@@ -38,6 +38,9 @@ RSpec.describe 'Settings applications page' do
       expect(page)
         .to have_content(I18n.t('doorkeeper.applications.index.title'))
         .and have_content('My new app')
+        .and have_content('read')
+        .and have_content('write')
+        .and have_content('follow')
     end
 
     it 'does not save with invalid form values' do
@@ -73,10 +76,12 @@ RSpec.describe 'Settings applications page' do
 
       fill_in form_app_name_label,
               with: 'My new app name with a new value'
+      check 'push', id: :doorkeeper_application_scopes_push
       submit_form
 
       expect(page)
         .to have_content('My new app name with a new value')
+        .and have_checked_field('push', id: :doorkeeper_application_scopes_push)
     end
 
     it 'does not update with wrong values' do
@@ -91,7 +96,7 @@ RSpec.describe 'Settings applications page' do
     end
 
     def submit_form
-      click_on I18n.t('generic.save_changes')
+      click_on(submit_button)
     end
   end
 
diff --git a/spec/system/settings/deletes_spec.rb b/spec/system/settings/deletes_spec.rb
new file mode 100644
index 0000000000..91f7104252
--- /dev/null
+++ b/spec/system/settings/deletes_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings Deletes' do
+  describe 'Deleting user from settings area' do
+    let(:user) { Fabricate(:user) }
+
+    before { sign_in(user) }
+
+    it 'requires password and deletes user record', :inline_jobs do
+      visit settings_delete_path
+      expect(page)
+        .to have_title(I18n.t('settings.delete'))
+        .and have_private_cache_control
+
+      # Wrong confirmation value
+      fill_in 'form_delete_confirmation_password', with: 'wrongvalue'
+      click_on I18n.t('deletes.proceed')
+      expect(page)
+        .to have_content(I18n.t('deletes.challenge_not_passed'))
+
+      # Correct confirmation value
+      fill_in 'form_delete_confirmation_password', with: user.password
+      click_on I18n.t('deletes.proceed')
+      expect(page)
+        .to have_content(I18n.t('deletes.success_msg'))
+      expect(page)
+        .to have_title(I18n.t('auth.login'))
+      expect(User.find_by(id: user.id))
+        .to be_nil
+      expect(user.account.reload)
+        .to be_suspended
+      expect(CanonicalEmailBlock.block?(user.email))
+        .to be(false)
+    end
+  end
+end
diff --git a/spec/system/settings/featured_tags_spec.rb b/spec/system/settings/featured_tags_spec.rb
new file mode 100644
index 0000000000..2460817f94
--- /dev/null
+++ b/spec/system/settings/featured_tags_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings Featured Tags' do
+  let(:user) { Fabricate(:user) }
+
+  before { sign_in(user) }
+
+  describe 'Managing tags' do
+    let(:tag) { Fabricate(:tag) }
+    let(:status) { Fabricate :status, account: user.account }
+
+    before { status.tags << tag }
+
+    it 'Views, adds, and removes featured tags' do
+      visit settings_featured_tags_path
+
+      # Link to existing tag used on a status
+      expect(page.body)
+        .to include(
+          settings_featured_tags_path(featured_tag: { name: tag.name })
+        )
+
+      # Invalid entry
+      fill_in 'featured_tag_name', with: 'test, #foo !bleh'
+      expect { click_on I18n.t('featured_tags.add_new') }
+        .to_not change(user.account.featured_tags, :count)
+
+      # Valid entry
+      fill_in 'featured_tag_name', with: '#friends'
+      expect { click_on I18n.t('featured_tags.add_new') }
+        .to change(user.account.featured_tags, :count).by(1)
+
+      # Delete the created entry
+      expect { click_on I18n.t('filters.index.delete') }
+        .to change(user.account.featured_tags, :count).by(-1)
+      expect(page)
+        .to have_title(I18n.t('settings.featured_tags'))
+    end
+  end
+end
diff --git a/spec/system/settings/migration/redirects_spec.rb b/spec/system/settings/migration/redirects_spec.rb
new file mode 100644
index 0000000000..b59be5ac1f
--- /dev/null
+++ b/spec/system/settings/migration/redirects_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings Migration Redirects' do
+  let!(:user) { Fabricate(:user, password: 'testtest') }
+
+  before { sign_in(user) }
+
+  describe 'Managing redirects' do
+    before { stub_resolver }
+
+    it 'creates and destroys redirects' do
+      visit new_settings_migration_redirect_path
+      expect(page)
+        .to have_title(I18n.t('settings.migrate'))
+
+      # Empty form invalid submission
+      expect { click_on I18n.t('migrations.set_redirect') }
+        .to_not(change { user.account.moved_to_account_id }.from(nil))
+
+      # Valid form submission
+      fill_in 'form_redirect_acct', with: 'new@example.host'
+      fill_in 'form_redirect_current_password', with: 'testtest'
+      expect { click_on I18n.t('migrations.set_redirect') }
+        .to(change { user.reload.account.moved_to_account_id }.from(nil))
+
+      # Delete the account move
+      expect { click_on I18n.t('migrations.cancel') }
+        .to(change { user.reload.account.moved_to_account_id }.to(nil))
+      expect(page)
+        .to have_content(I18n.t('migrations.cancelled_msg'))
+    end
+
+    private
+
+    def stub_resolver
+      resolver = instance_double(ResolveAccountService, call: Fabricate(:account))
+      allow(ResolveAccountService).to receive(:new).and_return(resolver)
+    end
+  end
+end
diff --git a/spec/system/settings/preferences/appearance_spec.rb b/spec/system/settings/preferences/appearance_spec.rb
index 32085e2af0..e8fb0c5de8 100644
--- a/spec/system/settings/preferences/appearance_spec.rb
+++ b/spec/system/settings/preferences/appearance_spec.rb
@@ -33,18 +33,18 @@ RSpec.describe 'Settings preferences appearance page' do
   end
 
   def confirm_delete_field
-    I18n.t('simple_form.labels.defaults.setting_delete_modal')
+    form_label('defaults.setting_delete_modal')
   end
 
   def confirm_reblog_field
-    I18n.t('simple_form.labels.defaults.setting_boost_modal')
+    form_label('defaults.setting_boost_modal')
   end
 
   def theme_selection_field
-    I18n.t('simple_form.labels.defaults.setting_theme')
+    form_label('defaults.setting_theme')
   end
 
   def advanced_layout_field
-    I18n.t('simple_form.labels.defaults.setting_advanced_layout')
+    form_label('defaults.setting_advanced_layout')
   end
 end
diff --git a/spec/system/settings/preferences/notifications_spec.rb b/spec/system/settings/preferences/notifications_spec.rb
index 20ff549222..c9d2c4270b 100644
--- a/spec/system/settings/preferences/notifications_spec.rb
+++ b/spec/system/settings/preferences/notifications_spec.rb
@@ -22,6 +22,6 @@ RSpec.describe 'Settings preferences notifications page' do
   end
 
   def notifications_follow_field
-    I18n.t('simple_form.labels.notification_emails.follow')
+    form_label('notification_emails.follow')
   end
 end
diff --git a/spec/system/settings/preferences/other_spec.rb b/spec/system/settings/preferences/other_spec.rb
index 7cc15f87a4..d741ec1ad2 100644
--- a/spec/system/settings/preferences/other_spec.rb
+++ b/spec/system/settings/preferences/other_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Settings preferences other page' do
   end
 
   def mark_sensitive_field
-    I18n.t('simple_form.labels.defaults.setting_default_sensitive')
+    form_label('defaults.setting_default_sensitive')
   end
 
   def language_field(key)
diff --git a/spec/system/settings/privacy_spec.rb b/spec/system/settings/privacy_spec.rb
new file mode 100644
index 0000000000..5e1498613e
--- /dev/null
+++ b/spec/system/settings/privacy_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings Privacy' do
+  let!(:user) { Fabricate(:user) }
+
+  before { sign_in(user) }
+
+  describe 'Managing privacy settings' do
+    before { user.account.update(discoverable: false) }
+
+    context 'with a successful update' do
+      it 'updates user profile information' do
+        # View settings page
+        visit settings_privacy_path
+        expect(page)
+          .to have_content(I18n.t('privacy.title'))
+          .and have_private_cache_control
+
+        # Fill out form and submit
+        check 'account_discoverable'
+        check 'account_indexable'
+        expect { click_on submit_button }
+          .to change { user.account.reload.discoverable }.to(true)
+        expect(page)
+          .to have_content(I18n.t('privacy.title'))
+          .and have_content(success_message)
+        expect(ActivityPub::UpdateDistributionWorker)
+          .to have_enqueued_sidekiq_job(user.account.id)
+      end
+    end
+
+    context 'with a failed update' do
+      before do
+        allow(UpdateAccountService).to receive(:new).and_return(failing_update_service)
+      end
+
+      it 'updates user profile information' do
+        # View settings page
+        visit settings_privacy_path
+        expect(page)
+          .to have_content(I18n.t('privacy.title'))
+          .and have_private_cache_control
+
+        # Fill out form and submit
+        check 'account_discoverable'
+        check 'account_indexable'
+        expect { click_on submit_button }
+          .to_not(change { user.account.reload.discoverable })
+        expect(page)
+          .to have_content(I18n.t('privacy.title'))
+        expect(ActivityPub::UpdateDistributionWorker)
+          .to_not have_enqueued_sidekiq_job(anything)
+      end
+
+      private
+
+      def failing_update_service
+        instance_double(UpdateAccountService, call: false)
+      end
+    end
+  end
+end
diff --git a/spec/system/settings/profiles_spec.rb b/spec/system/settings/profiles_spec.rb
index 73a5751141..23d9ab2fda 100644
--- a/spec/system/settings/profiles_spec.rb
+++ b/spec/system/settings/profiles_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe 'Settings profile page' do
   let(:account) { user.account }
 
   before do
-    allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
     sign_in user
   end
 
@@ -24,14 +23,14 @@ RSpec.describe 'Settings profile page' do
       .to change { account.reload.display_name }.to('New name')
       .and(change { account.reload.avatar.instance.avatar_file_name }.from(nil).to(be_present))
     expect(ActivityPub::UpdateDistributionWorker)
-      .to have_received(:perform_async).with(account.id)
+      .to have_enqueued_sidekiq_job(account.id)
   end
 
   def display_name_field
-    I18n.t('simple_form.labels.defaults.display_name')
+    form_label('defaults.display_name')
   end
 
   def avatar_field
-    I18n.t('simple_form.labels.defaults.avatar')
+    form_label('defaults.avatar')
   end
 end
diff --git a/spec/system/settings/sessions_spec.rb b/spec/system/settings/sessions_spec.rb
new file mode 100644
index 0000000000..ffc7a64185
--- /dev/null
+++ b/spec/system/settings/sessions_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings Sessions' do
+  let(:user) { Fabricate(:user) }
+  let!(:session_activation) { Fabricate(:session_activation, user: user) }
+
+  before { sign_in(user) }
+
+  describe 'deleting a session' do
+    it 'deletes listed session activation from the auth page' do
+      visit edit_user_registration_path
+      expect(page)
+        .to have_title(I18n.t('settings.account_settings'))
+
+      expect { click_on(I18n.t('sessions.revoke')) }
+        .to change(SessionActivation, :count).by(-1)
+      expect { session_activation.reload }
+        .to raise_error(ActiveRecord::RecordNotFound)
+      expect(page)
+        .to have_content(I18n.t('sessions.revoke_success'))
+    end
+  end
+end
diff --git a/spec/system/settings/two_factor_authentication/recovery_codes_spec.rb b/spec/system/settings/two_factor_authentication/recovery_codes_spec.rb
new file mode 100644
index 0000000000..ba8491429c
--- /dev/null
+++ b/spec/system/settings/two_factor_authentication/recovery_codes_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings TwoFactorAuthentication RecoveryCodes' do
+  describe 'Generating recovery codes' do
+    let(:user) { Fabricate :user, otp_required_for_login: true }
+    let(:backup_code) { +'147e7284c95bd260b91ed17820860019' }
+
+    before do
+      stub_code_generator
+      sign_in(user)
+    end
+
+    it 'updates the codes and includes them in the view' do
+      # Attempt to generate codes
+      visit settings_two_factor_authentication_methods_path
+      click_on I18n.t('two_factor_authentication.generate_recovery_codes')
+
+      # Fill in challenge password
+      fill_in 'form_challenge_current_password', with: user.password
+
+      expect { click_on I18n.t('challenge.confirm') }
+        .to(change { user.reload.otp_backup_codes })
+
+      expect(page)
+        .to have_content(I18n.t('two_factor_authentication.recovery_codes_regenerated'))
+        .and have_title(I18n.t('settings.two_factor_authentication'))
+        .and have_css('ol.recovery-codes')
+        .and have_content(backup_code)
+    end
+
+    def stub_code_generator
+      allow(SecureRandom).to receive(:hex).and_return(backup_code)
+    end
+  end
+end
diff --git a/spec/system/settings/two_factor_authentication_methods_spec.rb b/spec/system/settings/two_factor_authentication_methods_spec.rb
new file mode 100644
index 0000000000..bed226deb5
--- /dev/null
+++ b/spec/system/settings/two_factor_authentication_methods_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Settings TwoFactorAuthenticationMethods' do
+  context 'when signed in' do
+    let(:user) { Fabricate(:user) }
+
+    before { sign_in user }
+
+    describe 'Managing 2FA methods' do
+      before { user.update(otp_required_for_login: true) }
+
+      it 'disables 2FA with challenge confirmation', :inline_jobs do
+        visit settings_two_factor_authentication_methods_path
+        expect(page)
+          .to have_content(I18n.t('settings.two_factor_authentication'))
+          .and have_private_cache_control
+
+        # Attempt to disable
+        click_on I18n.t('two_factor_authentication.disable')
+        expect(page)
+          .to have_title(I18n.t('challenge.prompt'))
+
+        # Fill in challenge form
+        fill_in 'form_challenge_current_password', with: user.password
+        emails = capture_emails do
+          expect { click_on I18n.t('challenge.confirm') }
+            .to change { user.reload.otp_required_for_login }.to(false)
+        end
+
+        expect(page)
+          .to have_content(I18n.t('two_factor_authentication.disabled_success'))
+        expect(emails.first)
+          .to be_present
+          .and(deliver_to(user.email))
+          .and(have_subject(I18n.t('devise.mailer.two_factor_disabled.subject')))
+      end
+    end
+  end
+end
diff --git a/spec/system/settings/verifications_spec.rb b/spec/system/settings/verifications_spec.rb
index 26197f06f0..87220057ed 100644
--- a/spec/system/settings/verifications_spec.rb
+++ b/spec/system/settings/verifications_spec.rb
@@ -44,6 +44,6 @@ RSpec.describe 'Settings verification page' do
   end
 
   def attribution_field
-    I18n.t('simple_form.labels.account.attribution_domains')
+    form_label('account.attribution_domains')
   end
 end
diff --git a/spec/system/share_entrypoint_spec.rb b/spec/system/share_entrypoint_spec.rb
index 7ccfee599a..b55ea31657 100644
--- a/spec/system/share_entrypoint_spec.rb
+++ b/spec/system/share_entrypoint_spec.rb
@@ -23,24 +23,14 @@ RSpec.describe 'Share page', :js, :streaming do
     fill_in_form
 
     expect(page)
-      .to have_css('.notification-bar-message', text: translations['compose.published.body'])
+      .to have_css('.notification-bar-message', text: frontend_translations('compose.published.body'))
   end
 
   def fill_in_form
     within('.compose-form') do
-      fill_in translations['compose_form.placeholder'],
+      fill_in frontend_translations('compose_form.placeholder'),
               with: 'This is a new status!'
-      click_on translations['compose_form.publish']
+      click_on frontend_translations('compose_form.publish')
     end
   end
-
-  def translations
-    # TODO: Extract to system spec helper for re-use?
-    JSON.parse(
-      Rails
-        .root
-        .join('app', 'javascript', 'mastodon', 'locales', 'en.json')
-        .read
-    )
-  end
 end
diff --git a/spec/system/statuses_spec.rb b/spec/system/statuses_spec.rb
new file mode 100644
index 0000000000..704cae03f2
--- /dev/null
+++ b/spec/system/statuses_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Status page' do
+  let(:status) { Fabricate :status }
+
+  it 'visits the status page and renders the web app' do
+    visit short_account_status_path(account_username: status.account.username, id: status.id)
+
+    expect(page)
+      .to have_css('noscript', text: /Mastodon/)
+      .and have_css('body', class: 'app-body')
+  end
+end
diff --git a/spec/validators/date_of_birth_validator_spec.rb b/spec/validators/date_of_birth_validator_spec.rb
new file mode 100644
index 0000000000..33e69e811b
--- /dev/null
+++ b/spec/validators/date_of_birth_validator_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe DateOfBirthValidator do
+  let(:record_class) do
+    Class.new do
+      include ActiveModel::Validations
+
+      attr_accessor :date_of_birth
+
+      validates :date_of_birth, date_of_birth: true
+    end
+  end
+
+  let(:record) { record_class.new }
+
+  before do
+    Setting.min_age = 16
+  end
+
+  describe '#validate_each' do
+    context 'with an invalid date' do
+      it 'adds errors' do
+        record.date_of_birth = '76.830.10'
+
+        expect(record).to_not be_valid
+        expect(record.errors.first.attribute).to eq(:date_of_birth)
+        expect(record.errors.first.type).to eq(:invalid)
+      end
+    end
+
+    context 'with a date below age limit' do
+      it 'adds errors' do
+        record.date_of_birth = 13.years.ago.strftime('%d.%m.%Y')
+
+        expect(record).to_not be_valid
+        expect(record.errors.first.attribute).to eq(:date_of_birth)
+        expect(record.errors.first.type).to eq(:below_limit)
+      end
+    end
+
+    context 'with a date above age limit' do
+      it 'does not add errors' do
+        record.date_of_birth = 16.years.ago.strftime('%d.%m.%Y')
+
+        expect(record).to be_valid
+      end
+    end
+  end
+end
diff --git a/spec/validators/note_length_validator_spec.rb b/spec/validators/note_length_validator_spec.rb
index 3fdb4ae8b9..c761c95280 100644
--- a/spec/validators/note_length_validator_spec.rb
+++ b/spec/validators/note_length_validator_spec.rb
@@ -30,6 +30,22 @@ RSpec.describe NoteLengthValidator do
       expect(account.errors).to have_received(:add)
     end
 
+    it 'counts multi byte emoji as single character' do
+      text = '✨' * 500
+      account = instance_double(Account, note: text, errors: activemodel_errors)
+
+      subject.validate_each(account, 'note', text)
+      expect(account.errors).to_not have_received(:add)
+    end
+
+    it 'counts ZWJ sequence emoji as single character' do
+      text = '🏳️‍⚧️' * 500
+      account = instance_double(Account, note: text, errors: activemodel_errors)
+
+      subject.validate_each(account, 'note', text)
+      expect(account.errors).to_not have_received(:add)
+    end
+
     private
 
     def starting_string
diff --git a/spec/validators/poll_options_validator_spec.rb b/spec/validators/poll_options_validator_spec.rb
index 9e4ec744db..cc03e9d673 100644
--- a/spec/validators/poll_options_validator_spec.rb
+++ b/spec/validators/poll_options_validator_spec.rb
@@ -41,5 +41,31 @@ RSpec.describe PollOptionsValidator do
         expect(errors).to have_received(:add)
       end
     end
+
+    describe 'character length of poll options' do
+      context 'when poll has acceptable length options' do
+        let(:options) { %w(test this) }
+
+        it 'has no errors' do
+          expect(errors).to_not have_received(:add)
+        end
+      end
+
+      context 'when poll has multibyte and ZWJ emoji options' do
+        let(:options) { ['✨' * described_class::MAX_OPTION_CHARS, '🏳️‍⚧️' * described_class::MAX_OPTION_CHARS] }
+
+        it 'has no errors' do
+          expect(errors).to_not have_received(:add)
+        end
+      end
+
+      context 'when poll has options that are too long' do
+        let(:options) { ['ok', 'a' * (described_class::MAX_OPTION_CHARS**2)] }
+
+        it 'has errors' do
+          expect(errors).to have_received(:add)
+        end
+      end
+    end
   end
 end
diff --git a/spec/validators/status_length_validator_spec.rb b/spec/validators/status_length_validator_spec.rb
index ecbfd4ba37..050b7500bb 100644
--- a/spec/validators/status_length_validator_spec.rb
+++ b/spec/validators/status_length_validator_spec.rb
@@ -80,6 +80,22 @@ RSpec.describe StatusLengthValidator do
       subject.validate(status)
       expect(status.errors).to have_received(:add)
     end
+
+    it 'counts multi byte emoji as single character' do
+      text = '✨' * 500
+      status = status_double(text: text)
+
+      subject.validate(status)
+      expect(status.errors).to_not have_received(:add)
+    end
+
+    it 'counts ZWJ sequence emoji as single character' do
+      text = '🏳️‍⚧️' * 500
+      status = status_double(text: text)
+
+      subject.validate(status)
+      expect(status.errors).to_not have_received(:add)
+    end
   end
 
   private
diff --git a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb
new file mode 100644
index 0000000000..4746d742d0
--- /dev/null
+++ b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb
@@ -0,0 +1,281 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ActivityPub::FetchAllRepliesWorker do
+  subject { described_class.new }
+
+  let(:top_items) do
+    [
+      'http://example.com/self-reply-1',
+      'http://other.com/other-reply-2',
+      'http://example.com/self-reply-3',
+    ]
+  end
+
+  let(:top_items_paged) do
+    [
+      'http://example.com/self-reply-4',
+      'http://other.com/other-reply-5',
+      'http://example.com/self-reply-6',
+    ]
+  end
+
+  let(:nested_items) do
+    [
+      'http://example.com/nested-self-reply-1',
+      'http://other.com/nested-other-reply-2',
+      'http://example.com/nested-self-reply-3',
+    ]
+  end
+
+  let(:nested_items_paged) do
+    [
+      'http://example.com/nested-self-reply-4',
+      'http://other.com/nested-other-reply-5',
+      'http://example.com/nested-self-reply-6',
+    ]
+  end
+
+  let(:all_items) do
+    top_items + top_items_paged + nested_items + nested_items_paged
+  end
+
+  let(:top_note_uri) do
+    'http://example.com/top-post'
+  end
+
+  let(:top_collection_uri) do
+    'http://example.com/top-post/replies'
+  end
+
+  # The reply uri that has the nested replies under it
+  let(:reply_note_uri) do
+    'http://other.com/other-reply-2'
+  end
+
+  # The collection uri of nested replies
+  let(:reply_collection_uri) do
+    'http://other.com/other-reply-2/replies'
+  end
+
+  let(:replies_top) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: top_collection_uri,
+      type: 'Collection',
+      items: top_items + top_items_paged,
+    }
+  end
+
+  let(:replies_nested) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: reply_collection_uri,
+      type: 'Collection',
+      items: nested_items + nested_items_paged,
+    }
+  end
+
+  # The status resource for the top post
+  let(:top_object) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: top_note_uri,
+      type: 'Note',
+      content: 'Lorem ipsum',
+      replies: replies_top,
+      attributedTo: 'https://example.com',
+    }
+  end
+
+  # The status resource that has the uri to the replies collection
+  let(:reply_object) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: reply_note_uri,
+      type: 'Note',
+      content: 'Lorem ipsum',
+      replies: replies_nested,
+      attributedTo: 'https://other.com',
+    }
+  end
+
+  let(:empty_object) do
+    {
+      '@context': 'https://www.w3.org/ns/activitystreams',
+      id: 'https://example.com/empty',
+      type: 'Note',
+      content: 'Lorem ipsum',
+      replies: [],
+      attributedTo: 'https://example.com',
+    }
+  end
+
+  let(:account) { Fabricate(:account, domain: 'example.com') }
+  let(:status) do
+    Fabricate(
+      :status,
+      account: account,
+      uri: top_note_uri,
+      created_at: 1.day.ago - Status::FetchRepliesConcern::FETCH_REPLIES_INITIAL_WAIT_MINUTES
+    )
+  end
+
+  before do
+    stub_const('Status::FetchRepliesConcern::FETCH_REPLIES_ENABLED', true)
+    allow(FetchReplyWorker).to receive(:push_bulk)
+    all_items.each do |item|
+      next if [top_note_uri, reply_note_uri].include? item
+
+      stub_request(:get, item).to_return(status: 200, body: Oj.dump(empty_object), headers: { 'Content-Type': 'application/activity+json' })
+    end
+
+    stub_request(:get, top_note_uri).to_return(status: 200, body: Oj.dump(top_object), headers: { 'Content-Type': 'application/activity+json' })
+    stub_request(:get, reply_note_uri).to_return(status: 200, body: Oj.dump(reply_object), headers: { 'Content-Type': 'application/activity+json' })
+  end
+
+  shared_examples 'fetches all replies' do
+    it 'fetches statuses recursively' do
+      got_uris = subject.perform(status.id)
+      expect(got_uris).to match_array(all_items)
+    end
+
+    it 'respects the maximum limits set by not recursing after the max is reached' do
+      stub_const('ActivityPub::FetchAllRepliesWorker::MAX_REPLIES', 5)
+      got_uris = subject.perform(status.id)
+      expect(got_uris).to match_array(top_items + top_items_paged)
+    end
+  end
+
+  describe 'perform' do
+    context 'when the payload is a Note with replies as a Collection of inlined replies' do
+      it_behaves_like 'fetches all replies'
+    end
+
+    context 'when the payload is a Note with replies as a URI to a Collection' do
+      let(:top_object) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          id: top_note_uri,
+          type: 'Note',
+          content: 'Lorem ipsum',
+          replies: top_collection_uri,
+          attributedTo: 'https://example.com',
+        }
+      end
+      let(:reply_object) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          id: reply_note_uri,
+          type: 'Note',
+          content: 'Lorem ipsum',
+          replies: reply_collection_uri,
+          attributedTo: 'https://other.com',
+        }
+      end
+
+      before do
+        stub_request(:get, top_collection_uri).to_return(status: 200, body: Oj.dump(replies_top), headers: { 'Content-Type': 'application/activity+json' })
+        stub_request(:get, reply_collection_uri).to_return(status: 200, body: Oj.dump(replies_nested), headers: { 'Content-Type': 'application/activity+json' })
+      end
+
+      it_behaves_like 'fetches all replies'
+    end
+
+    context 'when the payload is a Note with replies as a paginated collection' do
+      let(:top_page_2_uri) do
+        "#{top_collection_uri}/2"
+      end
+
+      let(:reply_page_2_uri) do
+        "#{reply_collection_uri}/2"
+      end
+
+      let(:top_object) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          id: top_note_uri,
+          type: 'Note',
+          content: 'Lorem ipsum',
+          replies: {
+            type: 'Collection',
+            id: top_collection_uri,
+            first: {
+              type: 'CollectionPage',
+              partOf: top_collection_uri,
+              items: top_items,
+              next: top_page_2_uri,
+            },
+          },
+          attributedTo: 'https://example.com',
+        }
+      end
+      let(:reply_object) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          id: reply_note_uri,
+          type: 'Note',
+          content: 'Lorem ipsum',
+          replies: {
+            type: 'Collection',
+            id: reply_collection_uri,
+            first: {
+              type: 'CollectionPage',
+              partOf: reply_collection_uri,
+              items: nested_items,
+              next: reply_page_2_uri,
+            },
+          },
+          attributedTo: 'https://other.com',
+        }
+      end
+
+      let(:top_page_two) do
+        {
+          type: 'CollectionPage',
+          id: top_page_2_uri,
+          partOf: top_collection_uri,
+          items: top_items_paged,
+        }
+      end
+
+      let(:reply_page_two) do
+        {
+          type: 'CollectionPage',
+          id: reply_page_2_uri,
+          partOf: reply_collection_uri,
+          items: nested_items_paged,
+        }
+      end
+
+      before do
+        stub_request(:get, top_page_2_uri).to_return(status: 200, body: Oj.dump(top_page_two), headers: { 'Content-Type': 'application/activity+json' })
+        stub_request(:get, reply_page_2_uri).to_return(status: 200, body: Oj.dump(reply_page_two), headers: { 'Content-Type': 'application/activity+json' })
+      end
+
+      it_behaves_like 'fetches all replies'
+
+      it 'limits by max pages' do
+        stub_const('ActivityPub::FetchAllRepliesWorker::MAX_PAGES', 3)
+        got_uris = subject.perform(status.id)
+        expect(got_uris).to match_array(top_items + top_items_paged + nested_items)
+      end
+    end
+
+    context 'when replies should not be fetched' do
+      # ensure that we should not fetch by setting the status to be created in the debounce window
+      let(:status) { Fabricate(:status, account: account, uri: top_note_uri, created_at: DateTime.now) }
+
+      before do
+        stub_const('Status::FetchRepliesConcern::FETCH_REPLIES_INITIAL_WAIT_MINUTES', 1.week)
+      end
+
+      it 'returns nil without fetching' do
+        got_uris = subject.perform(status.id)
+        expect(got_uris).to be_nil
+        assert_not_requested :get, top_note_uri
+      end
+    end
+  end
+end
diff --git a/spec/workers/admin/distribute_announcement_notification_worker_spec.rb b/spec/workers/admin/distribute_announcement_notification_worker_spec.rb
new file mode 100644
index 0000000000..0e618418b0
--- /dev/null
+++ b/spec/workers/admin/distribute_announcement_notification_worker_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Admin::DistributeAnnouncementNotificationWorker do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    context 'with missing record' do
+      it 'runs without error' do
+        expect { worker.perform(nil) }.to_not raise_error
+      end
+    end
+
+    context 'with valid announcement' do
+      let(:announcement) { Fabricate(:announcement) }
+      let!(:user) { Fabricate :user, confirmed_at: 3.days.ago }
+
+      it 'sends the announcement via email', :inline_jobs do
+        emails = capture_emails { worker.perform(announcement.id) }
+
+        expect(emails.size)
+          .to eq(1)
+        expect(emails.first)
+          .to have_attributes(
+            to: [user.email],
+            subject: I18n.t('user_mailer.announcement_published.subject')
+          )
+      end
+    end
+  end
+end
diff --git a/spec/workers/import_worker_spec.rb b/spec/workers/import_worker_spec.rb
deleted file mode 100644
index 1d34aafe86..0000000000
--- a/spec/workers/import_worker_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe ImportWorker do
-  let(:worker) { described_class.new }
-  let(:service) { instance_double(ImportService, call: true) }
-
-  describe '#perform' do
-    before do
-      allow(ImportService).to receive(:new).and_return(service)
-    end
-
-    let(:import) { Fabricate(:import) }
-
-    it 'sends the import to the service' do
-      worker.perform(import.id)
-
-      expect(service).to have_received(:call).with(import)
-      expect { import.reload }.to raise_error(ActiveRecord::RecordNotFound)
-    end
-  end
-end
diff --git a/spec/workers/poll_expiration_notify_worker_spec.rb b/spec/workers/poll_expiration_notify_worker_spec.rb
index b3ccdd3d77..190630608c 100644
--- a/spec/workers/poll_expiration_notify_worker_spec.rb
+++ b/spec/workers/poll_expiration_notify_worker_spec.rb
@@ -33,15 +33,11 @@ RSpec.describe PollExpirationNotifyWorker do
       end
 
       context 'when poll is local' do
-        it 'notifies voters' do
+        it 'notifies voters, owner, and local voters' do
           expect(ActivityPub::DistributePollUpdateWorker).to have_enqueued_sidekiq_job(poll.status.id)
-        end
 
-        it 'notifies owner' do
           expect(LocalNotificationWorker).to have_enqueued_sidekiq_job(poll.account.id, poll.id, 'Poll', 'poll')
-        end
 
-        it 'notifies local voters' do
           expect(LocalNotificationWorker).to have_enqueued_sidekiq_job(poll_vote.account.id, poll.id, 'Poll', 'poll')
         end
       end
@@ -49,15 +45,11 @@ RSpec.describe PollExpirationNotifyWorker do
       context 'when poll is remote' do
         let(:remote?) { true }
 
-        it 'does not notify remote voters' do
+        it 'does not notify remote voters or owner, does notify local voters' do
           expect(ActivityPub::DistributePollUpdateWorker).to_not have_enqueued_sidekiq_job(poll.status.id)
-        end
 
-        it 'does not notify owner' do
           expect(LocalNotificationWorker).to_not have_enqueued_sidekiq_job(poll.account.id, poll.id, 'Poll', 'poll')
-        end
 
-        it 'notifies local voters' do
           expect(LocalNotificationWorker).to have_enqueued_sidekiq_job(poll_vote.account.id, poll.id, 'Poll', 'poll')
         end
       end
diff --git a/spec/workers/publish_scheduled_status_worker_spec.rb b/spec/workers/publish_scheduled_status_worker_spec.rb
index 9365e8a4bc..a91e665965 100644
--- a/spec/workers/publish_scheduled_status_worker_spec.rb
+++ b/spec/workers/publish_scheduled_status_worker_spec.rb
@@ -13,11 +13,9 @@ RSpec.describe PublishScheduledStatusWorker do
     end
 
     context 'when the account is not disabled' do
-      it 'creates a status' do
+      it 'creates a status and removes scheduled record' do
         expect(scheduled_status.account.statuses.first.text).to eq 'Hello world, future!'
-      end
 
-      it 'removes the scheduled status' do
         expect(ScheduledStatus.find_by(id: scheduled_status.id)).to be_nil
       end
     end
@@ -25,11 +23,9 @@ RSpec.describe PublishScheduledStatusWorker do
     context 'when the account is disabled' do
       let(:scheduled_status) { Fabricate(:scheduled_status, account: Fabricate(:account, user: Fabricate(:user, disabled: true))) }
 
-      it 'does not create a status' do
+      it 'does not create a status and removes scheduled record' do
         expect(Status.count).to eq 0
-      end
 
-      it 'removes the scheduled status' do
         expect(ScheduledStatus.find_by(id: scheduled_status.id)).to be_nil
       end
     end
diff --git a/spec/workers/unfilter_notifications_worker_spec.rb b/spec/workers/unfilter_notifications_worker_spec.rb
index 464a4520ff..2fd130301f 100644
--- a/spec/workers/unfilter_notifications_worker_spec.rb
+++ b/spec/workers/unfilter_notifications_worker_spec.rb
@@ -5,6 +5,7 @@ require 'rails_helper'
 RSpec.describe UnfilterNotificationsWorker do
   let(:recipient) { Fabricate(:account) }
   let(:sender) { Fabricate(:account) }
+  let(:worker) { described_class.new }
 
   before do
     # Populate multiple kinds of filtered notifications
@@ -67,23 +68,22 @@ RSpec.describe UnfilterNotificationsWorker do
   end
 
   describe '#perform' do
-    context 'with single argument (prerelease behavior)' do
-      subject { described_class.new.perform(notification_request.id) }
-
-      let(:notification_request) { Fabricate(:notification_request, from_account: sender, account: recipient) }
+    context 'with recipient and sender' do
+      subject { worker.perform(recipient.id, sender.id) }
 
       it_behaves_like 'shared behavior'
-
-      it 'destroys the notification request' do
-        expect { subject }
-          .to change { NotificationRequest.exists?(notification_request.id) }.to(false)
-      end
     end
 
-    context 'with two arguments' do
-      subject { described_class.new.perform(recipient.id, sender.id) }
+    context 'with missing records' do
+      it 'runs without error for missing sender' do
+        expect { worker.perform(recipient.id, nil) }
+          .to_not raise_error
+      end
 
-      it_behaves_like 'shared behavior'
+      it 'runs without error for missing recipient' do
+        expect { worker.perform(nil, sender.id) }
+          .to_not raise_error
+      end
     end
   end
 end
diff --git a/spec/workers/unfollow_follow_worker_spec.rb b/spec/workers/unfollow_follow_worker_spec.rb
index 7b9d49b902..2e05d1ca78 100644
--- a/spec/workers/unfollow_follow_worker_spec.rb
+++ b/spec/workers/unfollow_follow_worker_spec.rb
@@ -18,14 +18,11 @@ RSpec.describe UnfollowFollowWorker do
     let(:show_reblogs) { true }
 
     describe 'perform' do
-      it 'unfollows source account and follows target account' do
+      it 'unfollows source account and follows target account and preserves show_reblogs' do
         subject.perform(local_follower.id, source_account.id, target_account.id)
         expect(local_follower.following?(source_account)).to be false
         expect(local_follower.following?(target_account)).to be true
-      end
 
-      it 'preserves show_reblogs' do
-        subject.perform(local_follower.id, source_account.id, target_account.id)
         expect(Follow.find_by(account: local_follower, target_account: target_account).show_reblogs?).to be show_reblogs
       end
     end
@@ -35,14 +32,11 @@ RSpec.describe UnfollowFollowWorker do
     let(:show_reblogs) { false }
 
     describe 'perform' do
-      it 'unfollows source account and follows target account' do
+      it 'unfollows source account and follows target account and preserves show_reblogs' do
         subject.perform(local_follower.id, source_account.id, target_account.id)
         expect(local_follower.following?(source_account)).to be false
         expect(local_follower.following?(target_account)).to be true
-      end
 
-      it 'preserves show_reblogs' do
-        subject.perform(local_follower.id, source_account.id, target_account.id)
         expect(Follow.find_by(account: local_follower, target_account: target_account).show_reblogs?).to be show_reblogs
       end
     end
diff --git a/streaming/.eslintrc.cjs b/streaming/.eslintrc.cjs
deleted file mode 100644
index e25cff7df0..0000000000
--- a/streaming/.eslintrc.cjs
+++ /dev/null
@@ -1,43 +0,0 @@
-/* eslint-disable import/no-commonjs */
-
-// @ts-check
-
-// @ts-ignore - This needs to be a CJS file (eslint does not yet support ESM configs), and TS is complaining we use require
-const { defineConfig } = require('eslint-define-config');
-
-module.exports = defineConfig({
-  extends: ['../.eslintrc.js'],
-  env: {
-    browser: false,
-  },
-  parserOptions: {
-    project: true,
-    tsconfigRootDir: __dirname,
-    ecmaFeatures: {
-      jsx: false,
-    },
-    ecmaVersion: 2021,
-  },
-  rules: {
-    // In the streaming server we need to delete some variables to ensure
-    // garbage collection takes place on the values referenced by those objects;
-    // The alternative is to declare the variable as nullable, but then we need
-    // to assert it's in existence before every use, which becomes much harder
-    // to maintain.
-    'no-delete-var': 'off',
-
-    // This overrides the base configuration for this rule to pick up
-    // dependencies for the streaming server from the correct package.json file.
-    'import/no-extraneous-dependencies': [
-      'error',
-      {
-        devDependencies: ['streaming/.eslintrc.cjs'],
-        optionalDependencies: false,
-        peerDependencies: false,
-        includeTypes: true,
-        packageDir: __dirname,
-      },
-    ],
-    'import/extensions': ['error', 'always'],
-  },
-});
diff --git a/streaming/database.js b/streaming/database.js
index 60a3b34ef0..553c9149cc 100644
--- a/streaming/database.js
+++ b/streaming/database.js
@@ -49,7 +49,7 @@ export function configFromEnv(env, environment) {
     if (typeof parsedUrl.password === 'string') baseConfig.password = parsedUrl.password;
     if (typeof parsedUrl.host === 'string') baseConfig.host = parsedUrl.host;
     if (typeof parsedUrl.user === 'string') baseConfig.user = parsedUrl.user;
-    if (typeof parsedUrl.port === 'string') {
+    if (typeof parsedUrl.port === 'string' && parsedUrl.port) {
       const parsedPort = parseInt(parsedUrl.port, 10);
       if (isNaN(parsedPort)) {
         throw new Error('Invalid port specified in DATABASE_URL environment variable');
diff --git a/streaming/eslint.config.mjs b/streaming/eslint.config.mjs
new file mode 100644
index 0000000000..51a6551514
--- /dev/null
+++ b/streaming/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import globals from 'globals';
+import tseslint from 'typescript-eslint';
+
+// eslint-disable-next-line import/no-relative-packages -- Must import from the root
+import { baseConfig } from '../eslint.config.mjs';
+
+export default tseslint.config([
+  baseConfig,
+  {
+    languageOptions: {
+      globals: globals.node,
+
+      parser: tseslint.parser,
+      ecmaVersion: 2021,
+      sourceType: 'module',
+    },
+
+    settings: {
+      'import/ignore': ['node_modules', '\\.(json)$'],
+      'import/resolver': {
+        typescript: {},
+      },
+    },
+
+    rules: {
+      // In the streaming server we need to delete some variables to ensure
+      // garbage collection takes place on the values referenced by those objects;
+      // The alternative is to declare the variable as nullable, but then we need
+      // to assert it's in existence before every use, which becomes much harder
+      // to maintain.
+      'no-delete-var': 'off',
+
+      'import/no-extraneous-dependencies': [
+        'error',
+        {
+          devDependencies: ['**/*.config.mjs'],
+        },
+      ],
+
+      'import/extensions': ['error', 'always'],
+    },
+  },
+]);
diff --git a/streaming/index.js b/streaming/index.js
index 4b6817d8fd..ff3ef1c7ba 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -689,7 +689,7 @@ const startServer = async () => {
       // filtering of statuses:
 
       // Filter based on language:
-      if (Array.isArray(req.chosenLanguages) && payload.language !== null && req.chosenLanguages.indexOf(payload.language) === -1) {
+      if (Array.isArray(req.chosenLanguages) && req.chosenLanguages.indexOf(payload.language) === -1) {
         log.debug(`Message ${payload.id} filtered by language (${payload.language})`);
         return;
       }
diff --git a/streaming/lint-staged.config.mjs b/streaming/lint-staged.config.mjs
new file mode 100644
index 0000000000..430a999b9a
--- /dev/null
+++ b/streaming/lint-staged.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+  '*': 'prettier --ignore-unknown --write',
+  '*.{js,ts}': 'eslint --fix',
+  '**/*.ts': () => 'tsc -p tsconfig.json --noEmit',
+};
+
+export default config;
diff --git a/streaming/package.json b/streaming/package.json
index 4bcf85eeb2..fa33b575db 100644
--- a/streaming/package.json
+++ b/streaming/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@mastodon/streaming",
   "license": "AGPL-3.0-or-later",
-  "packageManager": "yarn@4.6.0",
+  "packageManager": "yarn@4.9.1",
   "engines": {
     "node": ">=18"
   },
@@ -31,14 +31,16 @@
     "ws": "^8.12.1"
   },
   "devDependencies": {
+    "@eslint/js": "^9.23.0",
     "@types/cors": "^2.8.16",
     "@types/express": "^4.17.17",
     "@types/pg": "^8.6.6",
     "@types/uuid": "^10.0.0",
     "@types/ws": "^8.5.9",
-    "eslint-define-config": "^2.0.0",
+    "globals": "^16.0.0",
     "pino-pretty": "^13.0.0",
-    "typescript": "^5.0.4"
+    "typescript": "~5.7.3",
+    "typescript-eslint": "^8.28.0"
   },
   "optionalDependencies": {
     "bufferutil": "^4.0.7",
diff --git a/streaming/tsconfig.json b/streaming/tsconfig.json
index 37e9a7fee0..2f892dc3b6 100644
--- a/streaming/tsconfig.json
+++ b/streaming/tsconfig.json
@@ -8,5 +8,5 @@
     "tsBuildInfoFile": "../tmp/cache/streaming/tsconfig.tsbuildinfo",
     "paths": {}
   },
-  "include": ["./*.js", "./.eslintrc.cjs"]
+  "include": ["./*.js"]
 }
diff --git a/yarn.lock b/yarn.lock
index d11e676432..fb750e3e11 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -55,7 +55,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.2":
+"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.26.2":
   version: 7.26.2
   resolution: "@babel/code-frame@npm:7.26.2"
   dependencies:
@@ -66,46 +66,59 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.26.5":
-  version: 7.26.5
-  resolution: "@babel/compat-data@npm:7.26.5"
-  checksum: 10c0/9d2b41f0948c3dfc5de44d9f789d2208c2ea1fd7eb896dfbb297fe955e696728d6f363c600cd211e7f58ccbc2d834fe516bb1e4cf883bbabed8a32b038afc1a0
+"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.26.5, @babel/compat-data@npm:^7.26.8":
+  version: 7.26.8
+  resolution: "@babel/compat-data@npm:7.26.8"
+  checksum: 10c0/66408a0388c3457fff1c2f6c3a061278dd7b3d2f0455ea29bb7b187fa52c60ae8b4054b3c0a184e21e45f0eaac63cf390737bc7504d1f4a088a6e7f652c068ca
   languageName: node
   linkType: hard
 
-"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.1, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.0":
-  version: 7.26.7
-  resolution: "@babel/core@npm:7.26.7"
+"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.1, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10":
+  version: 7.26.10
+  resolution: "@babel/core@npm:7.26.10"
   dependencies:
     "@ampproject/remapping": "npm:^2.2.0"
     "@babel/code-frame": "npm:^7.26.2"
-    "@babel/generator": "npm:^7.26.5"
+    "@babel/generator": "npm:^7.26.10"
     "@babel/helper-compilation-targets": "npm:^7.26.5"
     "@babel/helper-module-transforms": "npm:^7.26.0"
-    "@babel/helpers": "npm:^7.26.7"
-    "@babel/parser": "npm:^7.26.7"
-    "@babel/template": "npm:^7.25.9"
-    "@babel/traverse": "npm:^7.26.7"
-    "@babel/types": "npm:^7.26.7"
+    "@babel/helpers": "npm:^7.26.10"
+    "@babel/parser": "npm:^7.26.10"
+    "@babel/template": "npm:^7.26.9"
+    "@babel/traverse": "npm:^7.26.10"
+    "@babel/types": "npm:^7.26.10"
     convert-source-map: "npm:^2.0.0"
     debug: "npm:^4.1.0"
     gensync: "npm:^1.0.0-beta.2"
     json5: "npm:^2.2.3"
     semver: "npm:^6.3.1"
-  checksum: 10c0/fbd2cd9fc23280bdcaca556e558f715c0a42d940b9913c52582e8e3d24e391d269cb8a9cd6589172593983569021c379e28bba6b19ea2ee08674f6068c210a9d
+  checksum: 10c0/e046e0e988ab53841b512ee9d263ca409f6c46e2a999fe53024688b92db394346fa3aeae5ea0866331f62133982eee05a675d22922a4603c3f603aa09a581d62
   languageName: node
   linkType: hard
 
-"@babel/generator@npm:^7.26.5, @babel/generator@npm:^7.7.2":
-  version: 7.26.5
-  resolution: "@babel/generator@npm:7.26.5"
+"@babel/generator@npm:^7.26.10, @babel/generator@npm:^7.27.0":
+  version: 7.27.0
+  resolution: "@babel/generator@npm:7.27.0"
   dependencies:
-    "@babel/parser": "npm:^7.26.5"
-    "@babel/types": "npm:^7.26.5"
+    "@babel/parser": "npm:^7.27.0"
+    "@babel/types": "npm:^7.27.0"
     "@jridgewell/gen-mapping": "npm:^0.3.5"
     "@jridgewell/trace-mapping": "npm:^0.3.25"
     jsesc: "npm:^3.0.2"
-  checksum: 10c0/3be79e0aa03f38858a465d12ee2e468320b9122dc44fc85984713e32f16f4d77ce34a16a1a9505972782590e0b8d847b6f373621f9c6fafa1906d90f31416cb0
+  checksum: 10c0/7cb10693d2b365c278f109a745dc08856cae139d262748b77b70ce1d97da84627f79648cab6940d847392c0e5d180441669ed958b3aee98d9c7d274b37c553bd
+  languageName: node
+  linkType: hard
+
+"@babel/generator@npm:^7.7.2":
+  version: 7.26.9
+  resolution: "@babel/generator@npm:7.26.9"
+  dependencies:
+    "@babel/parser": "npm:^7.26.9"
+    "@babel/types": "npm:^7.26.9"
+    "@jridgewell/gen-mapping": "npm:^0.3.5"
+    "@jridgewell/trace-mapping": "npm:^0.3.25"
+    jsesc: "npm:^3.0.2"
+  checksum: 10c0/6b78872128205224a9a9761b9ea7543a9a7902a04b82fc2f6801ead4de8f59056bab3fd17b1f834ca7b049555fc4c79234b9a6230dd9531a06525306050becad
   languageName: node
   linkType: hard
 
@@ -142,19 +155,19 @@ __metadata:
   linkType: hard
 
 "@babel/helper-create-class-features-plugin@npm:^7.25.9":
-  version: 7.25.9
-  resolution: "@babel/helper-create-class-features-plugin@npm:7.25.9"
+  version: 7.27.0
+  resolution: "@babel/helper-create-class-features-plugin@npm:7.27.0"
   dependencies:
     "@babel/helper-annotate-as-pure": "npm:^7.25.9"
     "@babel/helper-member-expression-to-functions": "npm:^7.25.9"
     "@babel/helper-optimise-call-expression": "npm:^7.25.9"
-    "@babel/helper-replace-supers": "npm:^7.25.9"
+    "@babel/helper-replace-supers": "npm:^7.26.5"
     "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9"
-    "@babel/traverse": "npm:^7.25.9"
+    "@babel/traverse": "npm:^7.27.0"
     semver: "npm:^6.3.1"
   peerDependencies:
     "@babel/core": ^7.0.0
-  checksum: 10c0/b2bdd39f38056a76b9ba00ec5b209dd84f5c5ebd998d0f4033cf0e73d5f2c357fbb49d1ce52db77a2709fb29ee22321f84a5734dc9914849bdfee9ad12ce8caf
+  checksum: 10c0/c4945903136d934050e070f69a4d72ec425f1f70634e0ddf14ad36695f935125a6df559f8d5b94cc1ed49abd4ce9c5be8ef3ba033fa8d09c5dd78d1a9b97d8cc
   languageName: node
   linkType: hard
 
@@ -171,9 +184,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-define-polyfill-provider@npm:^0.6.1, @babel/helper-define-polyfill-provider@npm:^0.6.2":
-  version: 0.6.2
-  resolution: "@babel/helper-define-polyfill-provider@npm:0.6.2"
+"@babel/helper-define-polyfill-provider@npm:^0.6.1, @babel/helper-define-polyfill-provider@npm:^0.6.2, @babel/helper-define-polyfill-provider@npm:^0.6.3":
+  version: 0.6.3
+  resolution: "@babel/helper-define-polyfill-provider@npm:0.6.3"
   dependencies:
     "@babel/helper-compilation-targets": "npm:^7.22.6"
     "@babel/helper-plugin-utils": "npm:^7.22.5"
@@ -182,7 +195,7 @@ __metadata:
     resolve: "npm:^1.14.2"
   peerDependencies:
     "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0
-  checksum: 10c0/f777fe0ee1e467fdaaac059c39ed203bdc94ef2465fb873316e9e1acfc511a276263724b061e3b0af2f6d7ad3ff174f2bb368fde236a860e0f650fda43d7e022
+  checksum: 10c0/4320e3527645e98b6a0d5626fef815680e3b2b03ec36045de5e909b0f01546ab3674e96f50bf3bc8413f8c9037e5ee1a5f560ebdf8210426dad1c2c03c96184a
   languageName: node
   linkType: hard
 
@@ -228,7 +241,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.0, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.8.0":
+"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.8.0":
   version: 7.26.5
   resolution: "@babel/helper-plugin-utils@npm:7.26.5"
   checksum: 10c0/cdaba71d4b891aa6a8dfbe5bac2f94effb13e5fa4c2c487667fdbaa04eae059b78b28d85a885071f45f7205aeb56d16759e1bed9c118b94b16e4720ef1ab0f65
@@ -248,16 +261,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-replace-supers@npm:^7.25.9":
-  version: 7.25.9
-  resolution: "@babel/helper-replace-supers@npm:7.25.9"
+"@babel/helper-replace-supers@npm:^7.25.9, @babel/helper-replace-supers@npm:^7.26.5":
+  version: 7.26.5
+  resolution: "@babel/helper-replace-supers@npm:7.26.5"
   dependencies:
     "@babel/helper-member-expression-to-functions": "npm:^7.25.9"
     "@babel/helper-optimise-call-expression": "npm:^7.25.9"
-    "@babel/traverse": "npm:^7.25.9"
+    "@babel/traverse": "npm:^7.26.5"
   peerDependencies:
     "@babel/core": ^7.0.0
-  checksum: 10c0/0b40d7d2925bd3ba4223b3519e2e4d2456d471ad69aa458f1c1d1783c80b522c61f8237d3a52afc9e47c7174129bbba650df06393a6787d5722f2ec7f223c3f4
+  checksum: 10c0/b19b1245caf835207aaaaac3a494f03a16069ae55e76a2e1350b5acd560e6a820026997a8160e8ebab82ae873e8208759aa008eb8422a67a775df41f0a4633d4
   languageName: node
   linkType: hard
 
@@ -303,24 +316,35 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helpers@npm:^7.26.7":
-  version: 7.26.7
-  resolution: "@babel/helpers@npm:7.26.7"
+"@babel/helpers@npm:^7.26.10":
+  version: 7.26.10
+  resolution: "@babel/helpers@npm:7.26.10"
   dependencies:
-    "@babel/template": "npm:^7.25.9"
-    "@babel/types": "npm:^7.26.7"
-  checksum: 10c0/37fec398e53a2dbbf24bc2a025c4d571b2556cef18d8116d05d04b153f13ef659cdfbaab96c8eed875e629d39bdf9b3ea5d099ccf80544537de224e2d94f9b11
+    "@babel/template": "npm:^7.26.9"
+    "@babel/types": "npm:^7.26.10"
+  checksum: 10c0/f99e1836bcffce96db43158518bb4a24cf266820021f6461092a776cba2dc01d9fc8b1b90979d7643c5c2ab7facc438149064463a52dd528b21c6ab32509784f
   languageName: node
   linkType: hard
 
-"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.5, @babel/parser@npm:^7.26.7":
-  version: 7.26.7
-  resolution: "@babel/parser@npm:7.26.7"
+"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.26.9":
+  version: 7.26.9
+  resolution: "@babel/parser@npm:7.26.9"
   dependencies:
-    "@babel/types": "npm:^7.26.7"
+    "@babel/types": "npm:^7.26.9"
   bin:
     parser: ./bin/babel-parser.js
-  checksum: 10c0/dcb08a4f2878ece33caffefe43b71488d753324bae7ca58d64bca3bc4af34dcfa1b58abdf9972516d76af760fceb25bb9294ca33461d56b31c5059ccfe32001f
+  checksum: 10c0/4b9ef3c9a0d4c328e5e5544f50fe8932c36f8a2c851e7f14a85401487cd3da75cad72c2e1bcec1eac55599a6bbb2fdc091f274c4fcafa6bdd112d4915ff087fc
+  languageName: node
+  linkType: hard
+
+"@babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0":
+  version: 7.27.0
+  resolution: "@babel/parser@npm:7.27.0"
+  dependencies:
+    "@babel/types": "npm:^7.27.0"
+  bin:
+    parser: ./bin/babel-parser.js
+  checksum: 10c0/ba2ed3f41735826546a3ef2a7634a8d10351df221891906e59b29b0a0cd748f9b0e7a6f07576858a9de8e77785aad925c8389ddef146de04ea2842047c9d2859
   languageName: node
   linkType: hard
 
@@ -469,7 +493,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-syntax-jsx@npm:^7.25.0, @babel/plugin-syntax-jsx@npm:^7.25.9, @babel/plugin-syntax-jsx@npm:^7.7.2":
+"@babel/plugin-syntax-jsx@npm:^7.25.9, @babel/plugin-syntax-jsx@npm:^7.7.2":
   version: 7.25.9
   resolution: "@babel/plugin-syntax-jsx@npm:7.25.9"
   dependencies:
@@ -591,16 +615,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-transform-async-generator-functions@npm:^7.25.9":
-  version: 7.25.9
-  resolution: "@babel/plugin-transform-async-generator-functions@npm:7.25.9"
+"@babel/plugin-transform-async-generator-functions@npm:^7.26.8":
+  version: 7.26.8
+  resolution: "@babel/plugin-transform-async-generator-functions@npm:7.26.8"
   dependencies:
-    "@babel/helper-plugin-utils": "npm:^7.25.9"
+    "@babel/helper-plugin-utils": "npm:^7.26.5"
     "@babel/helper-remap-async-to-generator": "npm:^7.25.9"
-    "@babel/traverse": "npm:^7.25.9"
+    "@babel/traverse": "npm:^7.26.8"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 10c0/e3fcb9fc3d6ab6cbd4fcd956b48c17b5e92fe177553df266ffcd2b2c1f2f758b893e51b638e77ed867941e0436487d2b8b505908d615c41799241699b520dec6
+  checksum: 10c0/f6fefce963fe2e6268dde1958975d7adbce65fba94ca6f4bc554c90da03104ad1dd2e66d03bc0462da46868498428646e30b03a218ef0e5a84bfc87a7e375cec
   languageName: node
   linkType: hard
 
@@ -770,15 +794,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-transform-for-of@npm:^7.25.9":
-  version: 7.25.9
-  resolution: "@babel/plugin-transform-for-of@npm:7.25.9"
+"@babel/plugin-transform-for-of@npm:^7.26.9":
+  version: 7.26.9
+  resolution: "@babel/plugin-transform-for-of@npm:7.26.9"
   dependencies:
-    "@babel/helper-plugin-utils": "npm:^7.25.9"
+    "@babel/helper-plugin-utils": "npm:^7.26.5"
     "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 10c0/bf11abc71934a1f369f39cd7a33cf3d4dc5673026a53f70b7c1238c4fcc44e68b3ca1bdbe3db2076f60defb6ffe117cbe10b90f3e1a613b551d88f7c4e693bbe
+  checksum: 10c0/e28a521521cf9f84ddd69ca8da7c89fb9f7aa38e4dea35742fe973e4e1d7c23f9cee1a4861a2fdd9e9f18ff945886a44d7335cea1c603b96bfcb1c7c8791ef09
   languageName: node
   linkType: hard
 
@@ -1137,18 +1161,18 @@ __metadata:
   linkType: hard
 
 "@babel/plugin-transform-runtime@npm:^7.22.4":
-  version: 7.25.9
-  resolution: "@babel/plugin-transform-runtime@npm:7.25.9"
+  version: 7.26.9
+  resolution: "@babel/plugin-transform-runtime@npm:7.26.9"
   dependencies:
     "@babel/helper-module-imports": "npm:^7.25.9"
-    "@babel/helper-plugin-utils": "npm:^7.25.9"
+    "@babel/helper-plugin-utils": "npm:^7.26.5"
     babel-plugin-polyfill-corejs2: "npm:^0.4.10"
     babel-plugin-polyfill-corejs3: "npm:^0.10.6"
     babel-plugin-polyfill-regenerator: "npm:^0.6.1"
     semver: "npm:^6.3.1"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 10c0/888a4998ba0a2313de347954c9a8dfeccbff0633c69d33aee385b8878eba2b429dbfb00c3cc04f6bca454b9be8afa01ebbd73defb7fbbb6e2d3086205c07758b
+  checksum: 10c0/2c4d77d0671badc7fd53dcd7015df5db892712436c7e9740ffb2f5b85e8591e5bfe208f78dff402b4ee2d55d0f7a3c0a1102c683f333f4ee0cfa62f68ea68842
   languageName: node
   linkType: hard
 
@@ -1186,14 +1210,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-transform-template-literals@npm:^7.25.9":
-  version: 7.25.9
-  resolution: "@babel/plugin-transform-template-literals@npm:7.25.9"
+"@babel/plugin-transform-template-literals@npm:^7.26.8":
+  version: 7.26.8
+  resolution: "@babel/plugin-transform-template-literals@npm:7.26.8"
   dependencies:
-    "@babel/helper-plugin-utils": "npm:^7.25.9"
+    "@babel/helper-plugin-utils": "npm:^7.26.5"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 10c0/5144da6036807bbd4e9d2a8b92ae67a759543929f34f4db9b463448a77298f4a40bf1e92e582db208fe08ee116224806a3bd0bed75d9da404fc2c0af9e6da540
+  checksum: 10c0/205a938ded9554857a604416d369023a961334b6c20943bd861b45f0e5dbbeca1cf6fda1c2049126e38a0d18865993433fdc78eae3028e94836b3b643c08ba0d
   languageName: node
   linkType: hard
 
@@ -1271,10 +1295,10 @@ __metadata:
   linkType: hard
 
 "@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.22.4":
-  version: 7.26.7
-  resolution: "@babel/preset-env@npm:7.26.7"
+  version: 7.26.9
+  resolution: "@babel/preset-env@npm:7.26.9"
   dependencies:
-    "@babel/compat-data": "npm:^7.26.5"
+    "@babel/compat-data": "npm:^7.26.8"
     "@babel/helper-compilation-targets": "npm:^7.26.5"
     "@babel/helper-plugin-utils": "npm:^7.26.5"
     "@babel/helper-validator-option": "npm:^7.25.9"
@@ -1288,7 +1312,7 @@ __metadata:
     "@babel/plugin-syntax-import-attributes": "npm:^7.26.0"
     "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6"
     "@babel/plugin-transform-arrow-functions": "npm:^7.25.9"
-    "@babel/plugin-transform-async-generator-functions": "npm:^7.25.9"
+    "@babel/plugin-transform-async-generator-functions": "npm:^7.26.8"
     "@babel/plugin-transform-async-to-generator": "npm:^7.25.9"
     "@babel/plugin-transform-block-scoped-functions": "npm:^7.26.5"
     "@babel/plugin-transform-block-scoping": "npm:^7.25.9"
@@ -1303,7 +1327,7 @@ __metadata:
     "@babel/plugin-transform-dynamic-import": "npm:^7.25.9"
     "@babel/plugin-transform-exponentiation-operator": "npm:^7.26.3"
     "@babel/plugin-transform-export-namespace-from": "npm:^7.25.9"
-    "@babel/plugin-transform-for-of": "npm:^7.25.9"
+    "@babel/plugin-transform-for-of": "npm:^7.26.9"
     "@babel/plugin-transform-function-name": "npm:^7.25.9"
     "@babel/plugin-transform-json-strings": "npm:^7.25.9"
     "@babel/plugin-transform-literals": "npm:^7.25.9"
@@ -1331,7 +1355,7 @@ __metadata:
     "@babel/plugin-transform-shorthand-properties": "npm:^7.25.9"
     "@babel/plugin-transform-spread": "npm:^7.25.9"
     "@babel/plugin-transform-sticky-regex": "npm:^7.25.9"
-    "@babel/plugin-transform-template-literals": "npm:^7.25.9"
+    "@babel/plugin-transform-template-literals": "npm:^7.26.8"
     "@babel/plugin-transform-typeof-symbol": "npm:^7.26.7"
     "@babel/plugin-transform-unicode-escapes": "npm:^7.25.9"
     "@babel/plugin-transform-unicode-property-regex": "npm:^7.25.9"
@@ -1339,13 +1363,13 @@ __metadata:
     "@babel/plugin-transform-unicode-sets-regex": "npm:^7.25.9"
     "@babel/preset-modules": "npm:0.1.6-no-external-plugins"
     babel-plugin-polyfill-corejs2: "npm:^0.4.10"
-    babel-plugin-polyfill-corejs3: "npm:^0.10.6"
+    babel-plugin-polyfill-corejs3: "npm:^0.11.0"
     babel-plugin-polyfill-regenerator: "npm:^0.6.1"
-    core-js-compat: "npm:^3.38.1"
+    core-js-compat: "npm:^3.40.0"
     semver: "npm:^6.3.1"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 10c0/77d2e46a4f133768c5c8a6b3fec49a7c85c6baec601991e63458921e889ff93911f447723c3a99a6a471ca654ea6dc2aaa7ed690f3e518ee80cea7820ab80ce3
+  checksum: 10c0/6812ca76bd38165a58fe8354bab5e7204e1aa17d8b9270bd8f8babb08cc7fa94cd29525fe41b553f2ba0e84033d566f10da26012b8ee0f81897005c5225d0051
   languageName: node
   linkType: hard
 
@@ -1403,47 +1427,68 @@ __metadata:
   linkType: hard
 
 "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.23.7, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
-  version: 7.26.7
-  resolution: "@babel/runtime@npm:7.26.7"
+  version: 7.26.9
+  resolution: "@babel/runtime@npm:7.26.9"
   dependencies:
     regenerator-runtime: "npm:^0.14.0"
-  checksum: 10c0/60199c049f90e5e41c687687430052a370aca60bac7859ff4ee761c5c1739b8ba1604d391d01588c22dc0e93828cbadb8ada742578ad1b1df240746bce98729a
+  checksum: 10c0/e8517131110a6ec3a7360881438b85060e49824e007f4a64b5dfa9192cf2bb5c01e84bfc109f02d822c7edb0db926928dd6b991e3ee460b483fb0fac43152d9b
   languageName: node
   linkType: hard
 
-"@babel/template@npm:^7.25.9, @babel/template@npm:^7.3.3":
-  version: 7.25.9
-  resolution: "@babel/template@npm:7.25.9"
-  dependencies:
-    "@babel/code-frame": "npm:^7.25.9"
-    "@babel/parser": "npm:^7.25.9"
-    "@babel/types": "npm:^7.25.9"
-  checksum: 10c0/ebe677273f96a36c92cc15b7aa7b11cc8bc8a3bb7a01d55b2125baca8f19cae94ff3ce15f1b1880fb8437f3a690d9f89d4e91f16fc1dc4d3eb66226d128983ab
-  languageName: node
-  linkType: hard
-
-"@babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.7":
-  version: 7.26.7
-  resolution: "@babel/traverse@npm:7.26.7"
+"@babel/template@npm:^7.25.9, @babel/template@npm:^7.26.9, @babel/template@npm:^7.3.3":
+  version: 7.26.9
+  resolution: "@babel/template@npm:7.26.9"
   dependencies:
     "@babel/code-frame": "npm:^7.26.2"
-    "@babel/generator": "npm:^7.26.5"
-    "@babel/parser": "npm:^7.26.7"
-    "@babel/template": "npm:^7.25.9"
-    "@babel/types": "npm:^7.26.7"
-    debug: "npm:^4.3.1"
-    globals: "npm:^11.1.0"
-  checksum: 10c0/b23a36ce40d2e4970741431c45d4f92e3f4c2895c0a421456516b2729bd9e17278846e01ee3d9039b0adf5fc5a071768061c17fcad040e74a5c3e39517449d5b
+    "@babel/parser": "npm:^7.26.9"
+    "@babel/types": "npm:^7.26.9"
+  checksum: 10c0/019b1c4129cc01ad63e17529089c2c559c74709d225f595eee017af227fee11ae8a97a6ab19ae6768b8aa22d8d75dcb60a00b28f52e9fa78140672d928bc1ae9
   languageName: node
   linkType: hard
 
-"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.5, @babel/types@npm:^7.26.7, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
-  version: 7.26.7
-  resolution: "@babel/types@npm:7.26.7"
+"@babel/template@npm:^7.27.0":
+  version: 7.27.0
+  resolution: "@babel/template@npm:7.27.0"
+  dependencies:
+    "@babel/code-frame": "npm:^7.26.2"
+    "@babel/parser": "npm:^7.27.0"
+    "@babel/types": "npm:^7.27.0"
+  checksum: 10c0/13af543756127edb5f62bf121f9b093c09a2b6fe108373887ccffc701465cfbcb17e07cf48aa7f440415b263f6ec006e9415c79dfc2e8e6010b069435f81f340
+  languageName: node
+  linkType: hard
+
+"@babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.26.5, @babel/traverse@npm:^7.26.8, @babel/traverse@npm:^7.27.0":
+  version: 7.27.0
+  resolution: "@babel/traverse@npm:7.27.0"
+  dependencies:
+    "@babel/code-frame": "npm:^7.26.2"
+    "@babel/generator": "npm:^7.27.0"
+    "@babel/parser": "npm:^7.27.0"
+    "@babel/template": "npm:^7.27.0"
+    "@babel/types": "npm:^7.27.0"
+    debug: "npm:^4.3.1"
+    globals: "npm:^11.1.0"
+  checksum: 10c0/c7af29781960dacaae51762e8bc6c4b13d6ab4b17312990fbca9fc38e19c4ad7fecaae24b1cf52fb844e8e6cdc76c70ad597f90e496bcb3cc0a1d66b41a0aa5b
+  languageName: node
+  linkType: hard
+
+"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
+  version: 7.27.0
+  resolution: "@babel/types@npm:7.27.0"
   dependencies:
     "@babel/helper-string-parser": "npm:^7.25.9"
     "@babel/helper-validator-identifier": "npm:^7.25.9"
-  checksum: 10c0/7810a2bca97b13c253f07a0863a628d33dbe76ee3c163367f24be93bfaf4c8c0a325f73208abaaa050a6b36059efc2950c2e4b71fb109c0f07fa62221d8473d4
+  checksum: 10c0/6f1592eabe243c89a608717b07b72969be9d9d2fce1dee21426238757ea1fa60fdfc09b29de9e48d8104311afc6e6fb1702565a9cc1e09bc1e76f2b2ddb0f6e1
+  languageName: node
+  linkType: hard
+
+"@babel/types@npm:^7.26.9":
+  version: 7.26.9
+  resolution: "@babel/types@npm:7.26.9"
+  dependencies:
+    "@babel/helper-string-parser": "npm:^7.25.9"
+    "@babel/helper-validator-identifier": "npm:^7.25.9"
+  checksum: 10c0/999c56269ba00e5c57aa711fbe7ff071cd6990bafd1b978341ea7572cc78919986e2aa6ee51dacf4b6a7a6fa63ba4eb3f1a03cf55eee31b896a56d068b895964
   languageName: node
   linkType: hard
 
@@ -1464,33 +1509,33 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/color-helpers@npm:^5.0.1":
-  version: 5.0.1
-  resolution: "@csstools/color-helpers@npm:5.0.1"
-  checksum: 10c0/77fa3b7236eaa3f36dea24708ac0d5e53168903624ac5aed54615752a0730cd20773fda50e742ce868012eca8c000cc39688e05869e79f34714230ab6968d1e6
+"@csstools/color-helpers@npm:^5.0.2":
+  version: 5.0.2
+  resolution: "@csstools/color-helpers@npm:5.0.2"
+  checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b
   languageName: node
   linkType: hard
 
-"@csstools/css-calc@npm:^2.1.1":
-  version: 2.1.1
-  resolution: "@csstools/css-calc@npm:2.1.1"
+"@csstools/css-calc@npm:^2.1.1, @csstools/css-calc@npm:^2.1.2":
+  version: 2.1.2
+  resolution: "@csstools/css-calc@npm:2.1.2"
   peerDependencies:
     "@csstools/css-parser-algorithms": ^3.0.4
     "@csstools/css-tokenizer": ^3.0.3
-  checksum: 10c0/857c8dac40eb6ba8810408dad141bbcad060b28bce69dfd3bcf095a060fcaa23d5c4dbf52be88fcb57e12ce32c666e855dc68de1d8020851f6b432e3f9b29950
+  checksum: 10c0/34ced30553968ef5d5f9e00e3b90b48c47480cf130e282e99d57ec9b09f803aab8bc06325683e72a1518b5e7180a3da8b533f1b462062757c21989a53b482e1a
   languageName: node
   linkType: hard
 
-"@csstools/css-color-parser@npm:^3.0.7":
-  version: 3.0.7
-  resolution: "@csstools/css-color-parser@npm:3.0.7"
+"@csstools/css-color-parser@npm:^3.0.7, @csstools/css-color-parser@npm:^3.0.8":
+  version: 3.0.8
+  resolution: "@csstools/css-color-parser@npm:3.0.8"
   dependencies:
-    "@csstools/color-helpers": "npm:^5.0.1"
-    "@csstools/css-calc": "npm:^2.1.1"
+    "@csstools/color-helpers": "npm:^5.0.2"
+    "@csstools/css-calc": "npm:^2.1.2"
   peerDependencies:
     "@csstools/css-parser-algorithms": ^3.0.4
     "@csstools/css-tokenizer": ^3.0.3
-  checksum: 10c0/b81780e6c50f0b0605776bd39bbd6203780231a561601853a9835cc70788560e7a281d0fbfe47ebe8affcb07dd64b0b1dcd4b67552520cfbe0e5088df158f12c
+  checksum: 10c0/90722c5a62ca94e9d578ddf59be604a76400b932bd3d4bd23cb1ae9b7ace8fcf83c06995d2b31f96f4afef24a7cefba79beb11ed7ee4999d7ecfec3869368359
   languageName: node
   linkType: hard
 
@@ -1532,33 +1577,33 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-color-function@npm:^4.0.7":
-  version: 4.0.7
-  resolution: "@csstools/postcss-color-function@npm:4.0.7"
+"@csstools/postcss-color-function@npm:^4.0.8":
+  version: 4.0.8
+  resolution: "@csstools/postcss-color-function@npm:4.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
     "@csstools/utilities": "npm:^2.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/0f466e1d8863800f6428d7801e2134a834c9ea4b8098f84df41379cd3c3ba84f62588b46e03b26cf13c7d61b1112d22bdfd72adbcec7f5cb27f1149e855cd3ab
+  checksum: 10c0/d52c65bb4ed28f62b3fc9c0b2ce068e58395345dcead797ed8f7e4f5f469a9311607d39dd409c571ccc94d6c5c84171aff62d51d4f53fdcf6e1cca23fc31d4f1
   languageName: node
   linkType: hard
 
-"@csstools/postcss-color-mix-function@npm:^3.0.7":
-  version: 3.0.7
-  resolution: "@csstools/postcss-color-mix-function@npm:3.0.7"
+"@csstools/postcss-color-mix-function@npm:^3.0.8":
+  version: 3.0.8
+  resolution: "@csstools/postcss-color-mix-function@npm:3.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
     "@csstools/utilities": "npm:^2.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/e663615c7fde6effe9888c049bf74373c55d7d69e36c239eb1343c4aa86810b2407baebedd9fd67c6374fbecc32b4b96d11cdba6099473e4551ce7a1e9613513
+  checksum: 10c0/3fe7093b38f2b469462fa942af5a54a1ad68b07cd33267288e5c9e865d3a871c04774463136e4af24955316f40560dda1371d02cfd5595475a742afae13a37ba
   languageName: node
   linkType: hard
 
@@ -1576,16 +1621,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-exponential-functions@npm:^2.0.6":
-  version: 2.0.6
-  resolution: "@csstools/postcss-exponential-functions@npm:2.0.6"
+"@csstools/postcss-exponential-functions@npm:^2.0.7":
+  version: 2.0.7
+  resolution: "@csstools/postcss-exponential-functions@npm:2.0.7"
   dependencies:
-    "@csstools/css-calc": "npm:^2.1.1"
+    "@csstools/css-calc": "npm:^2.1.2"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/e8b5bdde8e60cdd628c6654f2336921fa0df1a9468ce3b7cd40c9f27457cd1f8a2ffc9c6380487d55c6188bed6e772679cefa4dfa5ba90229957a030df9403ce
+  checksum: 10c0/9d02076135ee9bf82bf911f577c9fda42bf00347f3c519fa83e32e83f5b8a98649b97e13ba3a42ed906467729d7b69574595556dfb9e865c86d3bbae5ffbc918
   languageName: node
   linkType: hard
 
@@ -1601,46 +1646,46 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-gamut-mapping@npm:^2.0.7":
-  version: 2.0.7
-  resolution: "@csstools/postcss-gamut-mapping@npm:2.0.7"
+"@csstools/postcss-gamut-mapping@npm:^2.0.8":
+  version: 2.0.8
+  resolution: "@csstools/postcss-gamut-mapping@npm:2.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/823603b1083ce2372ccbb2c25b744739cec8371ce593460a85896163fc8eb2b8e992497611af22dd765c2fccd8998b3d683732d61579d40bda0d3f21e6d74e06
+  checksum: 10c0/81daaba0e774ed3ab97e2c7c93dcae16d1e8447a27f0e82ddf8a176e8f1e93b444f463284105fd312c6234d4210372d6d69d96efcfb05bc5b6adfba6fcfd6f44
   languageName: node
   linkType: hard
 
-"@csstools/postcss-gradients-interpolation-method@npm:^5.0.7":
-  version: 5.0.7
-  resolution: "@csstools/postcss-gradients-interpolation-method@npm:5.0.7"
+"@csstools/postcss-gradients-interpolation-method@npm:^5.0.8":
+  version: 5.0.8
+  resolution: "@csstools/postcss-gradients-interpolation-method@npm:5.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
     "@csstools/utilities": "npm:^2.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/2998d28cd27ecf052da08679ca2fc5c8fcee68ade96cc32db4b4ae44f2b364954804e1e182cb547d6e8e4b5c84d6269763e12e3dfe1fd47c165c539c423b2ea0
+  checksum: 10c0/832bfb663b334be9783f49c354cbeec3cede1830a576b91a101456db33207e9651f97624f0df92e5d01a39b68a215ad4b20621ee229b92b51607e889093bc590
   languageName: node
   linkType: hard
 
-"@csstools/postcss-hwb-function@npm:^4.0.7":
-  version: 4.0.7
-  resolution: "@csstools/postcss-hwb-function@npm:4.0.7"
+"@csstools/postcss-hwb-function@npm:^4.0.8":
+  version: 4.0.8
+  resolution: "@csstools/postcss-hwb-function@npm:4.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
     "@csstools/utilities": "npm:^2.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/fa8e329ec92a9a04ba8d41d6640e39ea109c8d9ea1c90eaa141e303ae9bc41eca2c7bec72e4211f79a48b7e6746d754a66045b10da04ca9953c8a394a3bc1099
+  checksum: 10c0/d6196e2acfc0a6fd61fe254385049fb784abb862c724543940dbba8ffe29bbdbedd83985a517132a21073435445486f918da170fb0f710dbe40a798b9abc41e7
   languageName: node
   linkType: hard
 
@@ -1657,12 +1702,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-initial@npm:^2.0.0":
-  version: 2.0.0
-  resolution: "@csstools/postcss-initial@npm:2.0.0"
+"@csstools/postcss-initial@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "@csstools/postcss-initial@npm:2.0.1"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/44c443cba84cc66367f2082bf20db06c8437338c02c244c38798c5bf5342932d89fed0dd13e4409f084ecf7fce47ae6394e9a7a006fd98a973decfa24ab1eb04
+  checksum: 10c0/dbff7084ef4f1c4647efe2b147001daf172003c15b5e22689f0540d03c8d362f2a332cd9cf136e6c8dcda7564ee30492a4267ea188f72cb9c1000fb9bcfbfef8
   languageName: node
   linkType: hard
 
@@ -1742,17 +1787,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-media-minmax@npm:^2.0.6":
-  version: 2.0.6
-  resolution: "@csstools/postcss-media-minmax@npm:2.0.6"
+"@csstools/postcss-media-minmax@npm:^2.0.7":
+  version: 2.0.7
+  resolution: "@csstools/postcss-media-minmax@npm:2.0.7"
   dependencies:
-    "@csstools/css-calc": "npm:^2.1.1"
+    "@csstools/css-calc": "npm:^2.1.2"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/media-query-list-parser": "npm:^4.0.2"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/9cae49dcbba3f6818b679490665b48f13ab6c57f323a7e614e53a850503b6c5957a0de8dfff7184332c82f6f8570846283a96698791afb367457e4b24a4437f9
+  checksum: 10c0/03b7a5603437d5be17e9c0d951ca0b7b3b6f437fd4e24e3ac3f70ed9d573ef67641821fe209b5764c54aa36e841c830a5d8cf3a3dd97fd2fa774b7ceba7ba038
   languageName: node
   linkType: hard
 
@@ -1792,18 +1837,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-oklab-function@npm:^4.0.7":
-  version: 4.0.7
-  resolution: "@csstools/postcss-oklab-function@npm:4.0.7"
+"@csstools/postcss-oklab-function@npm:^4.0.8":
+  version: 4.0.8
+  resolution: "@csstools/postcss-oklab-function@npm:4.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
     "@csstools/utilities": "npm:^2.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/16d438aee2340dedd27249b540e261ea07bad56ceba507f6118e3eb44c693a977a374b554a1006a14c5d6d024f62d7cc468d7f4351a1c4e04e3a58142a3026a3
+  checksum: 10c0/8a62f3875bb9026c95758a0b834e876a8f07dd1a5ba36c3967e230565fbd9afd21ec714c8590cb4ea594fd214e68f2ccf58456ed6e919a47d2ed17d5b63a925a
   languageName: node
   linkType: hard
 
@@ -1818,31 +1863,31 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-random-function@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "@csstools/postcss-random-function@npm:1.0.2"
+"@csstools/postcss-random-function@npm:^1.0.3":
+  version: 1.0.3
+  resolution: "@csstools/postcss-random-function@npm:1.0.3"
   dependencies:
-    "@csstools/css-calc": "npm:^2.1.1"
+    "@csstools/css-calc": "npm:^2.1.2"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/b0bc235999685045ca91f8f18eb56ced68747aec6e8b7ff57ac86b1c385d6c2526a528fde5fd32e0987b4387b22a75c73e2d2ebd57974c4ca32d3d60a1eb093a
+  checksum: 10c0/c3bf319a6f79c0e372e4754e7888a4cd3a97b81e480662b1d1cb193949670bbcd5995c42483390a996e66d6dd81c9ad753836cc617aac2e3acbd542faa56f907
   languageName: node
   linkType: hard
 
-"@csstools/postcss-relative-color-syntax@npm:^3.0.7":
-  version: 3.0.7
-  resolution: "@csstools/postcss-relative-color-syntax@npm:3.0.7"
+"@csstools/postcss-relative-color-syntax@npm:^3.0.8":
+  version: 3.0.8
+  resolution: "@csstools/postcss-relative-color-syntax@npm:3.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
     "@csstools/utilities": "npm:^2.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/8741e3c337e5f321569fd41dac2442641390716bc67175faa3301bbbeaf23fe5b722b3b0b8f133ad0b927f32a2ed5fb73565fa8ee88685239d781f1826142405
+  checksum: 10c0/fcd14fb1c3f103dbaaf88afa2540f9946313d48515fa24fffcde4200e7dc4aa767d186ecf2e12bb0501dd946a824f118cd4ad5d44899c8d6d9d8d9d9b99a123e
   languageName: node
   linkType: hard
 
@@ -1857,54 +1902,54 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-sign-functions@npm:^1.1.1":
-  version: 1.1.1
-  resolution: "@csstools/postcss-sign-functions@npm:1.1.1"
+"@csstools/postcss-sign-functions@npm:^1.1.2":
+  version: 1.1.2
+  resolution: "@csstools/postcss-sign-functions@npm:1.1.2"
   dependencies:
-    "@csstools/css-calc": "npm:^2.1.1"
+    "@csstools/css-calc": "npm:^2.1.2"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/d9ebbbba833307aba0f490e527dd88a4796e94cc3ae0ba8ad1ada2830cdadfd3f9c9c37e18152903277905f847b71dac2ac1f9f78752aabc4c03a5c5c10157b1
+  checksum: 10c0/15a1c434c3059ab884634d32374d53265c0ea5b5d1f6cb979dcfef18903edbafbf334fcbabd5b24869356db93792adfe95d88efef998b7d6b4c6f4b8393faca1
   languageName: node
   linkType: hard
 
-"@csstools/postcss-stepped-value-functions@npm:^4.0.6":
-  version: 4.0.6
-  resolution: "@csstools/postcss-stepped-value-functions@npm:4.0.6"
+"@csstools/postcss-stepped-value-functions@npm:^4.0.7":
+  version: 4.0.7
+  resolution: "@csstools/postcss-stepped-value-functions@npm:4.0.7"
   dependencies:
-    "@csstools/css-calc": "npm:^2.1.1"
+    "@csstools/css-calc": "npm:^2.1.2"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/9da91f2ca041a4c4a3118c3ac92b9c9ae244442423bb2d20f6861c5e8225af8f7e05b0d794ae0600dd7a23ca565f7714e066e7a3ea180350534dc0a30ae0d7f4
+  checksum: 10c0/1e664f0b169abe0e8ad832844ff06b219702ba7e6af795801109bd2e90403295d5cdb2e27c17f92e60d9704b30726b4564da79e0bf66dec852d50704a8813053
   languageName: node
   linkType: hard
 
-"@csstools/postcss-text-decoration-shorthand@npm:^4.0.1":
-  version: 4.0.1
-  resolution: "@csstools/postcss-text-decoration-shorthand@npm:4.0.1"
+"@csstools/postcss-text-decoration-shorthand@npm:^4.0.2":
+  version: 4.0.2
+  resolution: "@csstools/postcss-text-decoration-shorthand@npm:4.0.2"
   dependencies:
-    "@csstools/color-helpers": "npm:^5.0.1"
+    "@csstools/color-helpers": "npm:^5.0.2"
     postcss-value-parser: "npm:^4.2.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/81950e248d6019c0066353895e0fa2a5c684b754c9af349218cb919534f5ebf79e5e9c7a10b3af1e9c56de2f246968de3b87a00d8c4102e5f88e0f05c04f9889
+  checksum: 10c0/01e2f3717e7a42224dc1a746491c55a381cf208cb7588f0308eeefe730675be4c7bb56c0cc557e75999c981e67da7d0b0bb68610635752c89ef251ee435b9cac
   languageName: node
   linkType: hard
 
-"@csstools/postcss-trigonometric-functions@npm:^4.0.6":
-  version: 4.0.6
-  resolution: "@csstools/postcss-trigonometric-functions@npm:4.0.6"
+"@csstools/postcss-trigonometric-functions@npm:^4.0.7":
+  version: 4.0.7
+  resolution: "@csstools/postcss-trigonometric-functions@npm:4.0.7"
   dependencies:
-    "@csstools/css-calc": "npm:^2.1.1"
+    "@csstools/css-calc": "npm:^2.1.2"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/b5aae978bbdca94c4a0f292ab5ec01750d8aeb62d68e0f9326b49ce94166886dfbfb48f169c9a053e874c9df78243053a30ceec91270a6022396d541d8c44ce9
+  checksum: 10c0/2b01608a9f7dba6f73febfdd75269f6f88eb2a653de38a0adc6e81de57de4248bedd39b3e8b219cc49ce73b99118e285a870711953a553ddddb0bd5b2f9a5852
   languageName: node
   linkType: hard
 
@@ -2016,6 +2061,41 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@emnapi/core@npm:^1.3.1":
+  version: 1.3.1
+  resolution: "@emnapi/core@npm:1.3.1"
+  dependencies:
+    "@emnapi/wasi-threads": "npm:1.0.1"
+    tslib: "npm:^2.4.0"
+  checksum: 10c0/d3be1044ad704e2c486641bc18908523490f28c7d38bd12d9c1d4ce37d39dae6c4aecd2f2eaf44c6e3bd90eaf04e0591acc440b1b038cdf43cce078a355a0ea0
+  languageName: node
+  linkType: hard
+
+"@emnapi/runtime@npm:^1.3.1":
+  version: 1.3.1
+  resolution: "@emnapi/runtime@npm:1.3.1"
+  dependencies:
+    tslib: "npm:^2.4.0"
+  checksum: 10c0/060ffede50f1b619c15083312b80a9e62a5b0c87aa8c1b54854c49766c9d69f8d1d3d87bd963a647071263a320db41b25eaa50b74d6a80dcc763c23dbeaafd6c
+  languageName: node
+  linkType: hard
+
+"@emnapi/wasi-threads@npm:1.0.1":
+  version: 1.0.1
+  resolution: "@emnapi/wasi-threads@npm:1.0.1"
+  dependencies:
+    tslib: "npm:^2.4.0"
+  checksum: 10c0/1e0c8036b8d53e9b07cc9acf021705ef6c86ab6b13e1acda7fffaf541a2d3565072afb92597419173ced9ea14f6bf32fce149106e669b5902b825e8b499e5c6c
+  languageName: node
+  linkType: hard
+
+"@emoji-mart/data@npm:1.2.1":
+  version: 1.2.1
+  resolution: "@emoji-mart/data@npm:1.2.1"
+  checksum: 10c0/6784b97bf49a0d3ff110d8447bbd3b0449fcbc497294be3d1c3a6cb1609308776895c7520200be604cbecaa5e172c76927e47f34419c72ba8a76fd4e5a53674b
+  languageName: node
+  linkType: hard
+
 "@emotion/babel-plugin@npm:^11.11.0":
   version: 11.11.0
   resolution: "@emotion/babel-plugin@npm:11.11.0"
@@ -2155,34 +2235,78 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.6.1":
-  version: 4.10.0
-  resolution: "@eslint-community/regexpp@npm:4.10.0"
-  checksum: 10c0/c5f60ef1f1ea7649fa7af0e80a5a79f64b55a8a8fa5086de4727eb4c86c652aedee407a9c143b8995d2c0b2d75c1222bec9ba5d73dbfc1f314550554f0979ef4
+"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1":
+  version: 4.12.1
+  resolution: "@eslint-community/regexpp@npm:4.12.1"
+  checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6
   languageName: node
   linkType: hard
 
-"@eslint/eslintrc@npm:^2.1.4":
-  version: 2.1.4
-  resolution: "@eslint/eslintrc@npm:2.1.4"
+"@eslint/config-array@npm:^0.19.2":
+  version: 0.19.2
+  resolution: "@eslint/config-array@npm:0.19.2"
+  dependencies:
+    "@eslint/object-schema": "npm:^2.1.6"
+    debug: "npm:^4.3.1"
+    minimatch: "npm:^3.1.2"
+  checksum: 10c0/dd68da9abb32d336233ac4fe0db1e15a0a8d794b6e69abb9e57545d746a97f6f542496ff9db0d7e27fab1438546250d810d90b1904ac67677215b8d8e7573f3d
+  languageName: node
+  linkType: hard
+
+"@eslint/config-helpers@npm:^0.2.0":
+  version: 0.2.0
+  resolution: "@eslint/config-helpers@npm:0.2.0"
+  checksum: 10c0/743a64653e13177029108f57ab47460ded08e3412c86216a14b7e8ab2dc79c2b64be45bf55c5ef29f83692a707dc34cf1e9217e4b8b4b272a0d9b691fdaf6a2a
+  languageName: node
+  linkType: hard
+
+"@eslint/core@npm:^0.12.0":
+  version: 0.12.0
+  resolution: "@eslint/core@npm:0.12.0"
+  dependencies:
+    "@types/json-schema": "npm:^7.0.15"
+  checksum: 10c0/d032af81195bb28dd800c2b9617548c6c2a09b9490da3c5537fd2a1201501666d06492278bb92cfccac1f7ac249e58601dd87f813ec0d6a423ef0880434fa0c3
+  languageName: node
+  linkType: hard
+
+"@eslint/eslintrc@npm:^3.3.1":
+  version: 3.3.1
+  resolution: "@eslint/eslintrc@npm:3.3.1"
   dependencies:
     ajv: "npm:^6.12.4"
     debug: "npm:^4.3.2"
-    espree: "npm:^9.6.0"
-    globals: "npm:^13.19.0"
+    espree: "npm:^10.0.1"
+    globals: "npm:^14.0.0"
     ignore: "npm:^5.2.0"
     import-fresh: "npm:^3.2.1"
     js-yaml: "npm:^4.1.0"
     minimatch: "npm:^3.1.2"
     strip-json-comments: "npm:^3.1.1"
-  checksum: 10c0/32f67052b81768ae876c84569ffd562491ec5a5091b0c1e1ca1e0f3c24fb42f804952fdd0a137873bc64303ba368a71ba079a6f691cee25beee9722d94cc8573
+  checksum: 10c0/b0e63f3bc5cce4555f791a4e487bf999173fcf27c65e1ab6e7d63634d8a43b33c3693e79f192cbff486d7df1be8ebb2bd2edc6e70ddd486cbfa84a359a3e3b41
   languageName: node
   linkType: hard
 
-"@eslint/js@npm:8.57.1":
-  version: 8.57.1
-  resolution: "@eslint/js@npm:8.57.1"
-  checksum: 10c0/b489c474a3b5b54381c62e82b3f7f65f4b8a5eaaed126546520bf2fede5532a8ed53212919fed1e9048dcf7f37167c8561d58d0ba4492a4244004e7793805223
+"@eslint/js@npm:9.23.0, @eslint/js@npm:^9.23.0":
+  version: 9.23.0
+  resolution: "@eslint/js@npm:9.23.0"
+  checksum: 10c0/4e70869372b6325389e0ab51cac6d3062689807d1cef2c3434857571422ce11dde3c62777af85c382b9f94d937127598d605d2086787f08611351bf99faded81
+  languageName: node
+  linkType: hard
+
+"@eslint/object-schema@npm:^2.1.6":
+  version: 2.1.6
+  resolution: "@eslint/object-schema@npm:2.1.6"
+  checksum: 10c0/b8cdb7edea5bc5f6a96173f8d768d3554a628327af536da2fc6967a93b040f2557114d98dbcdbf389d5a7b290985ad6a9ce5babc547f36fc1fde42e674d11a56
+  languageName: node
+  linkType: hard
+
+"@eslint/plugin-kit@npm:^0.2.7":
+  version: 0.2.7
+  resolution: "@eslint/plugin-kit@npm:0.2.7"
+  dependencies:
+    "@eslint/core": "npm:^0.12.0"
+    levn: "npm:^0.4.1"
+  checksum: 10c0/0a1aff1ad63e72aca923217e556c6dfd67d7cd121870eb7686355d7d1475d569773528a8b2111b9176f3d91d2ea81f7413c34600e8e5b73d59e005d70780b633
   languageName: node
   linkType: hard
 
@@ -2247,174 +2371,104 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/ecma402-abstract@npm:2.2.0":
-  version: 2.2.0
-  resolution: "@formatjs/ecma402-abstract@npm:2.2.0"
+"@formatjs/ecma402-abstract@npm:2.3.4":
+  version: 2.3.4
+  resolution: "@formatjs/ecma402-abstract@npm:2.3.4"
   dependencies:
-    "@formatjs/fast-memoize": "npm:2.2.1"
-    "@formatjs/intl-localematcher": "npm:0.5.5"
-    tslib: "npm:^2.7.0"
-  checksum: 10c0/3f8e5c1680ab3babf44a6324ae36d6355674562df61e0f0ce3bcec35c3b31d77f9c3b28596af79a9f52641aee834a11673dc4cf30093bbf27c4a96e18c8cc74b
+    "@formatjs/fast-memoize": "npm:2.2.7"
+    "@formatjs/intl-localematcher": "npm:0.6.1"
+    decimal.js: "npm:^10.4.3"
+    tslib: "npm:^2.8.0"
+  checksum: 10c0/2644bc618a34dc610ef9691281eeb45ae6175e6982cf19f1bd140672fc95c748747ce3c85b934649ea7e4a304f7ae0060625fd53d5df76f92ca3acf743e1eb0a
   languageName: node
   linkType: hard
 
-"@formatjs/ecma402-abstract@npm:2.3.2":
-  version: 2.3.2
-  resolution: "@formatjs/ecma402-abstract@npm:2.3.2"
+"@formatjs/fast-memoize@npm:2.2.7":
+  version: 2.2.7
+  resolution: "@formatjs/fast-memoize@npm:2.2.7"
   dependencies:
-    "@formatjs/fast-memoize": "npm:2.2.6"
-    "@formatjs/intl-localematcher": "npm:0.5.10"
-    decimal.js: "npm:10"
-    tslib: "npm:2"
-  checksum: 10c0/364e9e7de974fed976e0e8142a0f888ee0af4a11a61899115e5761ed933e7c1f16379b7b54a01524fd3c5d58bf08b71308237ea969cd54889eaf7bb2d30ec776
+    tslib: "npm:^2.8.0"
+  checksum: 10c0/f5eabb0e4ab7162297df8252b4cfde194b23248120d9df267592eae2be2d2f7c4f670b5a70523d91b4ecdc35d40e65823bb8eeba8dd79fbf8601a972bf3b8866
   languageName: node
   linkType: hard
 
-"@formatjs/fast-memoize@npm:2.2.1":
-  version: 2.2.1
-  resolution: "@formatjs/fast-memoize@npm:2.2.1"
+"@formatjs/icu-messageformat-parser@npm:2.11.2":
+  version: 2.11.2
+  resolution: "@formatjs/icu-messageformat-parser@npm:2.11.2"
   dependencies:
-    tslib: "npm:^2.7.0"
-  checksum: 10c0/cb8cbf1aba907d395d1fe405f67a8da26686b2fc26eefde7541d49d748d2d9b939f2bc428b7d40d2c31366a6ce42cf16724c966965701186986c5882fdba3c8b
+    "@formatjs/ecma402-abstract": "npm:2.3.4"
+    "@formatjs/icu-skeleton-parser": "npm:1.8.14"
+    tslib: "npm:^2.8.0"
+  checksum: 10c0/a121f2d2c6b36a1632ffd64c3545e2500c8ee0f7fee5db090318c035d635c430ab123faedb5d000f18d9423a7b55fbf670b84e2e2dd72cc307a38aed61d3b2e0
   languageName: node
   linkType: hard
 
-"@formatjs/fast-memoize@npm:2.2.6":
-  version: 2.2.6
-  resolution: "@formatjs/fast-memoize@npm:2.2.6"
+"@formatjs/icu-skeleton-parser@npm:1.8.14":
+  version: 1.8.14
+  resolution: "@formatjs/icu-skeleton-parser@npm:1.8.14"
   dependencies:
-    tslib: "npm:2"
-  checksum: 10c0/dccdc21105af673e58ec7b04eb17cd6fde1fb1a7e7a446273ca43f7ab97c26d5c0fcc2b9e80d5b54bf9b80354f9e1e681273c0ed26633ec72f0adc2d116dfd7f
+    "@formatjs/ecma402-abstract": "npm:2.3.4"
+    tslib: "npm:^2.8.0"
+  checksum: 10c0/a1807ed6e90b8a2e8d0e5b5125e6f9a2c057d3cff377fb031d2333af7cfaa6de4ed3a15c23da7294d4c3557f8b28b2163246434a19720f26b5db0497d97e9b58
   languageName: node
   linkType: hard
 
-"@formatjs/icu-messageformat-parser@npm:2.11.0":
-  version: 2.11.0
-  resolution: "@formatjs/icu-messageformat-parser@npm:2.11.0"
+"@formatjs/intl-localematcher@npm:0.6.1":
+  version: 0.6.1
+  resolution: "@formatjs/intl-localematcher@npm:0.6.1"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:2.3.2"
-    "@formatjs/icu-skeleton-parser": "npm:1.8.12"
-    tslib: "npm:2"
-  checksum: 10c0/9ad43847cb4a5c13895af606c634dcf2ec034d484cbbce6566746b60920643f33cbc5e2e3fd1efe21bcfdb555e1ee527e4518768001c3b36bf2e76c171e4049f
+    tslib: "npm:^2.8.0"
+  checksum: 10c0/bacbedd508519c1bb5ca2620e89dc38f12101be59439aa14aa472b222915b462cb7d679726640f6dcf52a05dd218b5aa27ccd60f2e5010bb96f1d4929848cde0
   languageName: node
   linkType: hard
 
-"@formatjs/icu-messageformat-parser@npm:2.7.10":
-  version: 2.7.10
-  resolution: "@formatjs/icu-messageformat-parser@npm:2.7.10"
+"@formatjs/intl-pluralrules@npm:^5.4.4":
+  version: 5.4.4
+  resolution: "@formatjs/intl-pluralrules@npm:5.4.4"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:2.2.0"
-    "@formatjs/icu-skeleton-parser": "npm:1.8.4"
-    tslib: "npm:^2.7.0"
-  checksum: 10c0/7f2b9bb31c64d2b45584a77e2e6a7c4185f247eea22c0d8e2257ac95feff36894a0936d31778d17874cd249c39058fcb894c27e13a6cadb056996e7232957655
+    "@formatjs/ecma402-abstract": "npm:2.3.4"
+    "@formatjs/intl-localematcher": "npm:0.6.1"
+    decimal.js: "npm:^10.4.3"
+    tslib: "npm:^2.8.0"
+  checksum: 10c0/8a8ec9f2fad40d9fa654a68de06fb18aaa6f0eafa908f41397f057366740625c12da627c6de68e0396fcd67ceaaa2c5c20a4b102f71ac8694abd9e76cceca949
   languageName: node
   linkType: hard
 
-"@formatjs/icu-skeleton-parser@npm:1.8.12":
-  version: 1.8.12
-  resolution: "@formatjs/icu-skeleton-parser@npm:1.8.12"
+"@formatjs/intl@npm:3.1.6":
+  version: 3.1.6
+  resolution: "@formatjs/intl@npm:3.1.6"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:2.3.2"
-    tslib: "npm:2"
-  checksum: 10c0/03e743aa09acb2137e37d03b98578fcbbc949d056b8c151763778e885d04d621e69c82f7656547f0532351d2a987bffac0a8c4c3d81186f47a28047ba64385e2
-  languageName: node
-  linkType: hard
-
-"@formatjs/icu-skeleton-parser@npm:1.8.4":
-  version: 1.8.4
-  resolution: "@formatjs/icu-skeleton-parser@npm:1.8.4"
-  dependencies:
-    "@formatjs/ecma402-abstract": "npm:2.2.0"
-    tslib: "npm:^2.7.0"
-  checksum: 10c0/a6cd90e89b994e6fa2b66f83a80250d17fdddaeb755cd1f38be1d51a737a6f8a9b123912943d3dcad278833e3a04683a9081b187300f1a481791527954e63e82
-  languageName: node
-  linkType: hard
-
-"@formatjs/intl-localematcher@npm:0.5.10":
-  version: 0.5.10
-  resolution: "@formatjs/intl-localematcher@npm:0.5.10"
-  dependencies:
-    tslib: "npm:2"
-  checksum: 10c0/362ec83aca9382165be575f1cefa477478339e6fead8ca8866185ce6e58427ea1487a811b12c73d1bcfa99fd4db0c24543b35c823451839f585576bfccb8c9cc
-  languageName: node
-  linkType: hard
-
-"@formatjs/intl-localematcher@npm:0.5.5":
-  version: 0.5.5
-  resolution: "@formatjs/intl-localematcher@npm:0.5.5"
-  dependencies:
-    tslib: "npm:^2.7.0"
-  checksum: 10c0/a6bf466bae29ca838ab06ffa2ce2cc9d5dd98e096ec73986b45ca4354b6adc0ca9078d9fd3aa30dbf27677940cfb3fb050ca0bce3018cd6f3f4d7e4bdad91035
-  languageName: node
-  linkType: hard
-
-"@formatjs/intl-pluralrules@npm:^5.2.2":
-  version: 5.4.2
-  resolution: "@formatjs/intl-pluralrules@npm:5.4.2"
-  dependencies:
-    "@formatjs/ecma402-abstract": "npm:2.3.2"
-    "@formatjs/intl-localematcher": "npm:0.5.10"
-    decimal.js: "npm:10"
-    tslib: "npm:2"
-  checksum: 10c0/0ecb9da19084d7a15e636362c206c7ee14297ad365e3e63ea53c82c8442d02cd4dcf8a0da65af9b0f835b32f0e8c3d43407d6ee0a0d7974c3044c92574b49686
-  languageName: node
-  linkType: hard
-
-"@formatjs/intl@npm:3.1.3":
-  version: 3.1.3
-  resolution: "@formatjs/intl@npm:3.1.3"
-  dependencies:
-    "@formatjs/ecma402-abstract": "npm:2.3.2"
-    "@formatjs/fast-memoize": "npm:2.2.6"
-    "@formatjs/icu-messageformat-parser": "npm:2.11.0"
-    intl-messageformat: "npm:10.7.14"
-    tslib: "npm:2"
+    "@formatjs/ecma402-abstract": "npm:2.3.4"
+    "@formatjs/fast-memoize": "npm:2.2.7"
+    "@formatjs/icu-messageformat-parser": "npm:2.11.2"
+    intl-messageformat: "npm:10.7.16"
+    tslib: "npm:^2.8.0"
   peerDependencies:
-    typescript: 5
+    typescript: ^5.6.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/1f9fce896b69dd3ff84221d2d18fbd57f8968c1d0691e44d33ac949d3d6f4e86f9c3075e4da870fb0b189d4952ec5e8bc7375558fa85c37bcb76a73aeb6f9f52
+  checksum: 10c0/a31f8d2569c9f2384f67a76f1cc2c8bfc2721c97a7dee0e971b6cfc0f223449bab0cfdc29140e3b71d74b04573c20ee8600909d256293e296a809da69a141530
   languageName: node
   linkType: hard
 
-"@formatjs/ts-transformer@npm:3.13.16":
-  version: 3.13.16
-  resolution: "@formatjs/ts-transformer@npm:3.13.16"
+"@formatjs/ts-transformer@npm:3.13.34":
+  version: 3.13.34
+  resolution: "@formatjs/ts-transformer@npm:3.13.34"
   dependencies:
-    "@formatjs/icu-messageformat-parser": "npm:2.7.10"
-    "@types/json-stable-stringify": "npm:^1.0.32"
-    "@types/node": "npm:14 || 16 || 17 || 18"
-    chalk: "npm:^4.0.0"
-    json-stable-stringify: "npm:^1.0.1"
-    tslib: "npm:^2.7.0"
-    typescript: "npm:5"
+    "@formatjs/icu-messageformat-parser": "npm:2.11.2"
+    "@types/json-stable-stringify": "npm:^1.1.0"
+    "@types/node": "npm:^22.0.0"
+    chalk: "npm:^4.1.2"
+    json-stable-stringify: "npm:^1.1.1"
+    tslib: "npm:^2.8.0"
+    typescript: "npm:^5.6.0"
   peerDependencies:
-    ts-jest: ">=27"
+    ts-jest: ^29
   peerDependenciesMeta:
     ts-jest:
       optional: true
-  checksum: 10c0/de03e8bc8c730f2e122970d5843762e0c5183181ba58c9759db2f1ed45fcbfbb8046dbb3318f906574f89026481b9205f5bd6fbf3f410a5e5e1754f575baa489
-  languageName: node
-  linkType: hard
-
-"@formatjs/ts-transformer@npm:3.13.31":
-  version: 3.13.31
-  resolution: "@formatjs/ts-transformer@npm:3.13.31"
-  dependencies:
-    "@formatjs/icu-messageformat-parser": "npm:2.11.0"
-    "@types/json-stable-stringify": "npm:1"
-    "@types/node": "npm:14 || 16 || 17 || 18 || 20 || 22"
-    chalk: "npm:4"
-    json-stable-stringify: "npm:1"
-    tslib: "npm:2"
-    typescript: "npm:5"
-  peerDependencies:
-    ts-jest: 27 || 28 || 29
-  peerDependenciesMeta:
-    ts-jest:
-      optional: true
-  checksum: 10c0/bfd7a41203c2b1691d883b3c306f3ae413b94c4875dec75df2be0e62da22625f673e41ecdc3257be40fe4a5b22a311365c2462d00a98f9bac59f00488543add0
+  checksum: 10c0/2e53af5a53cab71be0ba2fc16ba856c95bf336c063cc835486cd3a68d01013c5c08026b34667f4bdf99422e74faa8eb1f26d5fe8006f3a1ae9c77e065599362e
   languageName: node
   linkType: hard
 
@@ -2459,14 +2513,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@humanwhocodes/config-array@npm:^0.13.0":
-  version: 0.13.0
-  resolution: "@humanwhocodes/config-array@npm:0.13.0"
+"@humanfs/core@npm:^0.19.1":
+  version: 0.19.1
+  resolution: "@humanfs/core@npm:0.19.1"
+  checksum: 10c0/aa4e0152171c07879b458d0e8a704b8c3a89a8c0541726c6b65b81e84fd8b7564b5d6c633feadc6598307d34564bd53294b533491424e8e313d7ab6c7bc5dc67
+  languageName: node
+  linkType: hard
+
+"@humanfs/node@npm:^0.16.6":
+  version: 0.16.6
+  resolution: "@humanfs/node@npm:0.16.6"
   dependencies:
-    "@humanwhocodes/object-schema": "npm:^2.0.3"
-    debug: "npm:^4.3.1"
-    minimatch: "npm:^3.0.5"
-  checksum: 10c0/205c99e756b759f92e1f44a3dc6292b37db199beacba8f26c2165d4051fe73a4ae52fdcfd08ffa93e7e5cb63da7c88648f0e84e197d154bbbbe137b2e0dd332e
+    "@humanfs/core": "npm:^0.19.1"
+    "@humanwhocodes/retry": "npm:^0.3.0"
+  checksum: 10c0/8356359c9f60108ec204cbd249ecd0356667359b2524886b357617c4a7c3b6aace0fd5a369f63747b926a762a88f8a25bc066fa1778508d110195ce7686243e1
   languageName: node
   linkType: hard
 
@@ -2477,10 +2537,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@humanwhocodes/object-schema@npm:^2.0.3":
-  version: 2.0.3
-  resolution: "@humanwhocodes/object-schema@npm:2.0.3"
-  checksum: 10c0/80520eabbfc2d32fe195a93557cef50dfe8c8905de447f022675aaf66abc33ae54098f5ea78548d925aa671cd4ab7c7daa5ad704fe42358c9b5e7db60f80696c
+"@humanwhocodes/retry@npm:^0.3.0":
+  version: 0.3.1
+  resolution: "@humanwhocodes/retry@npm:0.3.1"
+  checksum: 10c0/f0da1282dfb45e8120480b9e2e275e2ac9bbe1cf016d046fdad8e27cc1285c45bb9e711681237944445157b430093412b4446c1ab3fc4bb037861b5904101d3b
+  languageName: node
+  linkType: hard
+
+"@humanwhocodes/retry@npm:^0.4.2":
+  version: 0.4.2
+  resolution: "@humanwhocodes/retry@npm:0.4.2"
+  checksum: 10c0/0235525d38f243bee3bf8b25ed395fbf957fb51c08adae52787e1325673071abe856c7e18e530922ed2dd3ce12ed82ba01b8cee0279ac52a3315fcdc3a69ef0c
   languageName: node
   linkType: hard
 
@@ -2823,20 +2890,24 @@ __metadata:
     "@dnd-kit/core": "npm:^6.1.0"
     "@dnd-kit/sortable": "npm:^10.0.0"
     "@dnd-kit/utilities": "npm:^3.2.2"
+    "@emoji-mart/data": "npm:1.2.1"
+    "@eslint/js": "npm:^9.23.0"
     "@formatjs/cli": "npm:^6.1.1"
-    "@formatjs/intl-pluralrules": "npm:^5.2.2"
+    "@formatjs/intl-pluralrules": "npm:^5.4.4"
     "@gamestdio/websocket": "npm:^0.3.2"
     "@github/webauthn-json": "npm:^2.1.1"
     "@hello-pangea/dnd": "npm:^17.0.0"
     "@rails/ujs": "npm:7.1.501"
+    "@react-spring/web": "npm:^9.7.5"
     "@reduxjs/toolkit": "npm:^2.0.1"
     "@svgr/webpack": "npm:^5.5.0"
     "@testing-library/dom": "npm:^10.2.0"
     "@testing-library/jest-dom": "npm:^6.0.0"
     "@testing-library/react": "npm:^16.0.0"
     "@types/babel__core": "npm:^7.20.1"
-    "@types/emoji-mart": "npm:^3.0.9"
+    "@types/emoji-mart": "npm:3.0.14"
     "@types/escape-html": "npm:^1.0.2"
+    "@types/eslint-plugin-jsx-a11y": "npm:^6"
     "@types/hoist-non-react-statics": "npm:^3.3.1"
     "@types/http-link-header": "npm:^1.0.3"
     "@types/intl": "npm:^1.2.0"
@@ -2851,7 +2922,6 @@ __metadata:
     "@types/react-dom": "npm:^18.2.4"
     "@types/react-helmet": "npm:^6.1.6"
     "@types/react-immutable-proptypes": "npm:^2.1.0"
-    "@types/react-motion": "npm:^0.0.40"
     "@types/react-router": "npm:^5.1.20"
     "@types/react-router-dom": "npm:^5.3.3"
     "@types/react-sparklines": "npm:^1.7.2"
@@ -2862,14 +2932,13 @@ __metadata:
     "@types/requestidlecallback": "npm:^0.3.5"
     "@types/webpack": "npm:^4.41.33"
     "@types/webpack-env": "npm:^1.18.4"
-    "@typescript-eslint/eslint-plugin": "npm:^8.0.0"
-    "@typescript-eslint/parser": "npm:^8.0.0"
+    "@use-gesture/react": "npm:^10.3.1"
     arrow-key-navigation: "npm:^1.2.0"
     async-mutex: "npm:^0.5.0"
     axios: "npm:^1.4.0"
     babel-jest: "npm:^29.5.0"
     babel-loader: "npm:^8.3.0"
-    babel-plugin-formatjs: "npm:^10.5.1"
+    babel-plugin-formatjs: "npm:^10.5.37"
     babel-plugin-lodash: "patch:babel-plugin-lodash@npm%3A3.3.4#~/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch"
     babel-plugin-preval: "npm:^5.1.0"
     babel-plugin-transform-react-remove-prop-types: "npm:^0.4.24"
@@ -2886,26 +2955,26 @@ __metadata:
     detect-passive-events: "npm:^2.0.3"
     emoji-mart: "npm:emoji-mart-lazyload@latest"
     escape-html: "npm:^1.0.3"
-    eslint: "npm:^8.41.0"
-    eslint-define-config: "npm:^2.0.0"
-    eslint-import-resolver-typescript: "npm:^3.5.5"
-    eslint-plugin-formatjs: "npm:^5.0.0"
-    eslint-plugin-import: "npm:~2.30.0"
-    eslint-plugin-jsdoc: "npm:^50.0.0"
-    eslint-plugin-jsx-a11y: "npm:~6.10.0"
-    eslint-plugin-promise: "npm:~7.1.0"
-    eslint-plugin-react: "npm:^7.33.2"
-    eslint-plugin-react-hooks: "npm:^5.0.0"
+    eslint: "npm:^9.23.0"
+    eslint-import-resolver-typescript: "npm:^4.2.5"
+    eslint-plugin-formatjs: "npm:^5.3.1"
+    eslint-plugin-import: "npm:~2.31.0"
+    eslint-plugin-jsdoc: "npm:^50.6.9"
+    eslint-plugin-jsx-a11y: "npm:~6.10.2"
+    eslint-plugin-promise: "npm:~7.2.1"
+    eslint-plugin-react: "npm:^7.37.4"
+    eslint-plugin-react-hooks: "npm:^5.2.0"
     file-loader: "npm:^6.2.0"
     fuzzysort: "npm:^3.0.0"
     glob: "npm:^10.2.6"
+    globals: "npm:^16.0.0"
     history: "npm:^4.10.1"
     hoist-non-react-statics: "npm:^3.3.2"
     http-link-header: "npm:^1.1.1"
     husky: "npm:^9.0.11"
     immutable: "npm:^4.3.0"
     imports-loader: "npm:^1.2.0"
-    intl-messageformat: "npm:^10.3.5"
+    intl-messageformat: "npm:^10.7.16"
     jest: "npm:^29.5.0"
     jest-environment-jsdom: "npm:^29.5.0"
     js-yaml: "npm:^4.1.0"
@@ -2928,9 +2997,7 @@ __metadata:
     react-hotkeys: "npm:^1.1.4"
     react-immutable-proptypes: "npm:^2.2.0"
     react-immutable-pure-component: "npm:^2.2.2"
-    react-intl: "npm:^7.0.0"
-    react-motion: "npm:^0.5.2"
-    react-notification: "npm:^6.8.5"
+    react-intl: "npm:^7.1.10"
     react-overlays: "npm:^5.2.1"
     react-redux: "npm:^9.0.4"
     react-redux-loading-bar: "npm:^5.0.8"
@@ -2958,7 +3025,8 @@ __metadata:
     tesseract.js: "npm:^6.0.0"
     tiny-queue: "npm:^0.2.1"
     twitter-text: "npm:3.1.0"
-    typescript: "npm:^5.0.4"
+    typescript: "npm:~5.7.3"
+    typescript-eslint: "npm:^8.28.0"
     use-debounce: "npm:^10.0.0"
     webpack: "npm:^4.47.0"
     webpack-assets-manifest: "npm:^4.0.6"
@@ -2987,6 +3055,7 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@mastodon/streaming@workspace:streaming"
   dependencies:
+    "@eslint/js": "npm:^9.23.0"
     "@types/cors": "npm:^2.8.16"
     "@types/express": "npm:^4.17.17"
     "@types/pg": "npm:^8.6.6"
@@ -2995,8 +3064,8 @@ __metadata:
     bufferutil: "npm:^4.0.7"
     cors: "npm:^2.8.5"
     dotenv: "npm:^16.0.3"
-    eslint-define-config: "npm:^2.0.0"
     express: "npm:^4.18.2"
+    globals: "npm:^16.0.0"
     ioredis: "npm:^5.3.2"
     jsdom: "npm:^26.0.0"
     pg: "npm:^8.5.0"
@@ -3005,7 +3074,8 @@ __metadata:
     pino-http: "npm:^10.0.0"
     pino-pretty: "npm:^13.0.0"
     prom-client: "npm:^15.0.0"
-    typescript: "npm:^5.0.4"
+    typescript: "npm:~5.7.3"
+    typescript-eslint: "npm:^8.28.0"
     utf-8-validate: "npm:^6.0.3"
     uuid: "npm:^11.0.0"
     ws: "npm:^8.12.1"
@@ -3017,6 +3087,17 @@ __metadata:
   languageName: unknown
   linkType: soft
 
+"@napi-rs/wasm-runtime@npm:^0.2.7":
+  version: 0.2.7
+  resolution: "@napi-rs/wasm-runtime@npm:0.2.7"
+  dependencies:
+    "@emnapi/core": "npm:^1.3.1"
+    "@emnapi/runtime": "npm:^1.3.1"
+    "@tybys/wasm-util": "npm:^0.9.0"
+  checksum: 10c0/04a5edd79144bfa4e821a373fb6d4939f10c578c5f3633b5e67a57d0f5e36a593f595834d26654ea757bba7cd80b6c42d0d1405d6a8460c5d774e8cd5c9548a4
+  languageName: node
+  linkType: hard
+
 "@nodelib/fs.scandir@npm:2.1.5":
   version: 2.1.5
   resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -3034,7 +3115,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8":
+"@nodelib/fs.walk@npm:^1.2.3":
   version: 1.2.8
   resolution: "@nodelib/fs.walk@npm:1.2.8"
   dependencies:
@@ -3044,13 +3125,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@nolyfill/is-core-module@npm:1.0.39":
-  version: 1.0.39
-  resolution: "@nolyfill/is-core-module@npm:1.0.39"
-  checksum: 10c0/34ab85fdc2e0250879518841f74a30c276bca4f6c3e13526d2d1fe515e1adf6d46c25fcd5989d22ea056d76f7c39210945180b4859fc83b050e2da411aa86289
-  languageName: node
-  linkType: hard
-
 "@npmcli/agent@npm:^2.0.0":
   version: 2.2.0
   resolution: "@npmcli/agent@npm:2.2.0"
@@ -3279,9 +3353,75 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@react-spring/animated@npm:~9.7.5":
+  version: 9.7.5
+  resolution: "@react-spring/animated@npm:9.7.5"
+  dependencies:
+    "@react-spring/shared": "npm:~9.7.5"
+    "@react-spring/types": "npm:~9.7.5"
+  peerDependencies:
+    react: ^16.8.0 || ^17.0.0 || ^18.0.0
+  checksum: 10c0/f8c2473c60f39a878c7dd0fdfcfcdbc720521e1506aa3f63c9de64780694a0a73d5ccc535a5ccec3520ddb70a71cf43b038b32c18e99531522da5388c510ecd7
+  languageName: node
+  linkType: hard
+
+"@react-spring/core@npm:~9.7.5":
+  version: 9.7.5
+  resolution: "@react-spring/core@npm:9.7.5"
+  dependencies:
+    "@react-spring/animated": "npm:~9.7.5"
+    "@react-spring/shared": "npm:~9.7.5"
+    "@react-spring/types": "npm:~9.7.5"
+  peerDependencies:
+    react: ^16.8.0 || ^17.0.0 || ^18.0.0
+  checksum: 10c0/5bfd83dfe248cd91889f215f015d908c7714ef445740fd5afa054b27ebc7d5a456abf6c309e2459d9b5b436e78d6fda16b62b9601f96352e9130552c02270830
+  languageName: node
+  linkType: hard
+
+"@react-spring/rafz@npm:~9.7.5":
+  version: 9.7.5
+  resolution: "@react-spring/rafz@npm:9.7.5"
+  checksum: 10c0/8bdad180feaa9a0e870a513043a5e98a4e9b7292a9f887575b7e6fadab2677825bc894b7ff16c38511b35bfe6cc1072df5851c5fee64448d67551559578ca759
+  languageName: node
+  linkType: hard
+
+"@react-spring/shared@npm:~9.7.5":
+  version: 9.7.5
+  resolution: "@react-spring/shared@npm:9.7.5"
+  dependencies:
+    "@react-spring/rafz": "npm:~9.7.5"
+    "@react-spring/types": "npm:~9.7.5"
+  peerDependencies:
+    react: ^16.8.0 || ^17.0.0 || ^18.0.0
+  checksum: 10c0/0207eacccdedd918a2fc55e78356ce937f445ce27ad9abd5d3accba8f9701a39349b55115641dc2b39bb9d3a155b058c185b411d292dc8cc5686bfa56f73b94f
+  languageName: node
+  linkType: hard
+
+"@react-spring/types@npm:~9.7.5":
+  version: 9.7.5
+  resolution: "@react-spring/types@npm:9.7.5"
+  checksum: 10c0/85c05121853cacb64f7cf63a4855e9044635e1231f70371cd7b8c78bc10be6f4dd7c68f592f92a2607e8bb68051540989b4677a2ccb525dba937f5cd95dc8bc1
+  languageName: node
+  linkType: hard
+
+"@react-spring/web@npm:^9.7.5":
+  version: 9.7.5
+  resolution: "@react-spring/web@npm:9.7.5"
+  dependencies:
+    "@react-spring/animated": "npm:~9.7.5"
+    "@react-spring/core": "npm:~9.7.5"
+    "@react-spring/shared": "npm:~9.7.5"
+    "@react-spring/types": "npm:~9.7.5"
+  peerDependencies:
+    react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+  checksum: 10c0/bcd1e052e1b16341a12a19bf4515f153ca09d1fa86ff7752a5d02d7c4db58e8baf80e6283e64411f1e388c65340dce2254b013083426806b5dbae38bd151e53e
+  languageName: node
+  linkType: hard
+
 "@reduxjs/toolkit@npm:^2.0.1":
-  version: 2.5.1
-  resolution: "@reduxjs/toolkit@npm:2.5.1"
+  version: 2.6.1
+  resolution: "@reduxjs/toolkit@npm:2.6.1"
   dependencies:
     immer: "npm:^10.0.3"
     redux: "npm:^5.0.1"
@@ -3295,7 +3435,7 @@ __metadata:
       optional: true
     react-redux:
       optional: true
-  checksum: 10c0/e25dd4085e5611d21d4e8d47716072e12318ef8171323d40a80c5b8e79e6d514a973718eb44e41f8491355f7a15e488a0e9f88a97c237327de2615a00b470929
+  checksum: 10c0/6ae5db267f2b0a9da8b59080797c5adb1b92b50c0f2bdd933470206285d684b79ed2b1c4e2d7a0fb4aa9e3772d0e215fdcd3a8e4a4a2aac81400fcd790dbd6cd
   languageName: node
   linkType: hard
 
@@ -3643,6 +3783,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@tybys/wasm-util@npm:^0.9.0":
+  version: 0.9.0
+  resolution: "@tybys/wasm-util@npm:0.9.0"
+  dependencies:
+    tslib: "npm:^2.4.0"
+  checksum: 10c0/f9fde5c554455019f33af6c8215f1a1435028803dc2a2825b077d812bed4209a1a64444a4ca0ce2ea7e1175c8d88e2f9173a36a33c199e8a5c671aa31de8242d
+  languageName: node
+  linkType: hard
+
 "@types/aria-query@npm:^5.0.1":
   version: 5.0.4
   resolution: "@types/aria-query@npm:5.0.4"
@@ -3728,7 +3877,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/emoji-mart@npm:^3.0.9":
+"@types/emoji-mart@npm:3.0.14":
   version: 3.0.14
   resolution: "@types/emoji-mart@npm:3.0.14"
   dependencies:
@@ -3744,7 +3893,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/eslint@npm:9":
+"@types/eslint-plugin-jsx-a11y@npm:^6":
+  version: 6.10.0
+  resolution: "@types/eslint-plugin-jsx-a11y@npm:6.10.0"
+  dependencies:
+    "@types/eslint": "npm:*"
+  checksum: 10c0/ec494e0ea56a0a10140316a74463eb8a6ac5ed02d7408ef91444f7c0c9bc875866a9d0be7a761f1631ea543c2413b1db947bf7cb1df9d936171e5d82f71c6772
+  languageName: node
+  linkType: hard
+
+"@types/eslint@npm:*, @types/eslint@npm:^9.6.1":
   version: 9.6.1
   resolution: "@types/eslint@npm:9.6.1"
   dependencies:
@@ -3754,10 +3912,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/estree@npm:*, @types/estree@npm:^1.0.0":
-  version: 1.0.5
-  resolution: "@types/estree@npm:1.0.5"
-  checksum: 10c0/b3b0e334288ddb407c7b3357ca67dbee75ee22db242ca7c56fe27db4e1a31989cb8af48a84dd401deb787fe10cc6b2ab1ee82dc4783be87ededbe3d53c79c70d
+"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6":
+  version: 1.0.7
+  resolution: "@types/estree@npm:1.0.7"
+  checksum: 10c0/be815254316882f7c40847336cd484c3bc1c3e34f710d197160d455dc9d6d050ffbf4c3bc76585dba86f737f020ab20bdb137ebe0e9116b0c86c7c0342221b8c
   languageName: node
   linkType: hard
 
@@ -3818,7 +3976,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/hoist-non-react-statics@npm:3, @types/hoist-non-react-statics@npm:^3.3.1":
+"@types/hoist-non-react-statics@npm:^3.3.1":
   version: 3.3.6
   resolution: "@types/hoist-non-react-statics@npm:3.3.6"
   dependencies:
@@ -3904,14 +4062,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8":
+"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8":
   version: 7.0.15
   resolution: "@types/json-schema@npm:7.0.15"
   checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
   languageName: node
   linkType: hard
 
-"@types/json-stable-stringify@npm:1, @types/json-stable-stringify@npm:^1.0.32":
+"@types/json-stable-stringify@npm:^1.1.0":
   version: 1.1.0
   resolution: "@types/json-stable-stringify@npm:1.1.0"
   checksum: 10c0/8f69944701510243cd3a83aa44363a8a4d366f11a659b258f69fb3ad0f94ab1e2533206a2c929ac7fd18784d201b663b3f02a45934f545c926f051d8cb4df095
@@ -3926,9 +4084,9 @@ __metadata:
   linkType: hard
 
 "@types/lodash@npm:^4.14.195":
-  version: 4.17.13
-  resolution: "@types/lodash@npm:4.17.13"
-  checksum: 10c0/c3d0b7efe7933ac0369b99f2f7bff9240d960680fdb74b41ed4bd1b3ca60cca1e31fe4046d9abbde778f941a41bc2a75eb629abf8659fa6c27b66efbbb0802a9
+  version: 4.17.16
+  resolution: "@types/lodash@npm:4.17.16"
+  checksum: 10c0/cf017901b8ab1d7aabc86d5189d9288f4f99f19a75caf020c0e2c77b8d4cead4db0d0b842d009b029339f92399f49f34377dd7c2721053388f251778b4c23534
   languageName: node
   linkType: hard
 
@@ -3953,21 +4111,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/node@npm:*, @types/node@npm:14 || 16 || 17 || 18 || 20 || 22":
-  version: 22.8.6
-  resolution: "@types/node@npm:22.8.6"
+"@types/node@npm:*, @types/node@npm:^22.0.0":
+  version: 22.13.14
+  resolution: "@types/node@npm:22.13.14"
   dependencies:
-    undici-types: "npm:~6.19.8"
-  checksum: 10c0/d3a11f2549234a91a4c5d0ff35ab4bdcb7ba34db4d3f1d189be39b8bd41c19aac98d117150a95a9c5a9d45b1014135477ea240b2b8317c86ae3d3cf1c3b3f8f4
-  languageName: node
-  linkType: hard
-
-"@types/node@npm:14 || 16 || 17 || 18":
-  version: 18.19.55
-  resolution: "@types/node@npm:18.19.55"
-  dependencies:
-    undici-types: "npm:~5.26.4"
-  checksum: 10c0/19ffeca0086b45cba08d4585623cd0d80fbacb659debde82a4baa008fc0c25ba6c21cd721f3a9f0be74f70940694b00458cac61c89f8b2a1e55ca4322a9aad2b
+    undici-types: "npm:~6.20.0"
+  checksum: 10c0/fa2ab5b8277bfbcc86c42e46a3ea9871b0d559894cc9d955685d17178c9499f0b1bf03d1d1ea8d92ef2dda818988f4035acb8abf9dc15423a998fa56173ab804
   languageName: node
   linkType: hard
 
@@ -3986,20 +4135,20 @@ __metadata:
   linkType: hard
 
 "@types/pg@npm:^8.6.6":
-  version: 8.11.10
-  resolution: "@types/pg@npm:8.11.10"
+  version: 8.11.11
+  resolution: "@types/pg@npm:8.11.11"
   dependencies:
     "@types/node": "npm:*"
     pg-protocol: "npm:*"
     pg-types: "npm:^4.0.1"
-  checksum: 10c0/c8800d0ab2c6424308e6c6b40c73f19583ee1aed758462bd07694844b0a551b5841442205a4ee05207b80109ba502f33f20241b1bd9b4902e713611fb9e08f6c
+  checksum: 10c0/18c2585e1ba7a5dd5f849d49410d53fdfe9a6c3cbc4ae46c51fd728264d6ecf9a84a5cd82d89cb1f870a74383bad88effce1eed888f16accbcbde56a53d23a69
   languageName: node
   linkType: hard
 
-"@types/picomatch@npm:^2.3.0":
-  version: 2.3.2
-  resolution: "@types/picomatch@npm:2.3.2"
-  checksum: 10c0/91445cfc0d07fe2a44c16ee284ab2e2a279da3f6df9c62ad61e7bc50343e47bef541369aff6110c4e51bd8fe501fc9c564deefbb4c03e392254889de6b46f237
+"@types/picomatch@npm:^3":
+  version: 3.0.2
+  resolution: "@types/picomatch@npm:3.0.2"
+  checksum: 10c0/f35d16fe10a6e13ead6499dd7d7d317e4fd78e48260398104e837e5ca83d393024bdc6f432cb644c0a69b0726a071fcc6eb09befbbcfafb3c3c5f71dbbfde487
   languageName: node
   linkType: hard
 
@@ -4073,15 +4222,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/react-motion@npm:^0.0.40":
-  version: 0.0.40
-  resolution: "@types/react-motion@npm:0.0.40"
-  dependencies:
-    "@types/react": "npm:*"
-  checksum: 10c0/8a560051be917833fdbe051185b53aeafbe8657968ac8e073ac874b9a55c6f16e3793748b13cfb9bd6d9a3d27aba116d6f8f296ec1950f4175dc94d17c5e8470
-  languageName: node
-  linkType: hard
-
 "@types/react-router-dom@npm:^5.3.3":
   version: 5.3.3
   resolution: "@types/react-router-dom@npm:5.3.3"
@@ -4269,9 +4409,9 @@ __metadata:
   linkType: hard
 
 "@types/webpack-env@npm:^1.18.4":
-  version: 1.18.5
-  resolution: "@types/webpack-env@npm:1.18.5"
-  checksum: 10c0/b9e4876e8c7cae419896249f9ed795db283c008fe1d38efa679cbbf05194fc2eea2a5bfb4ff4393d109e3a9895416dadf5f3ddd5c22931b678062230f860454e
+  version: 1.18.8
+  resolution: "@types/webpack-env@npm:1.18.8"
+  checksum: 10c0/527a5d1eb75c5243e4f3665d956c7c340f899955dd25d16c9fd9750406f32e95a3a17d207640295038e8235c0c2a2daf084f420e088e58b965d82fc74f6012d7
   languageName: node
   linkType: hard
 
@@ -4301,11 +4441,11 @@ __metadata:
   linkType: hard
 
 "@types/ws@npm:^8.5.9":
-  version: 8.5.13
-  resolution: "@types/ws@npm:8.5.13"
+  version: 8.5.14
+  resolution: "@types/ws@npm:8.5.14"
   dependencies:
     "@types/node": "npm:*"
-  checksum: 10c0/a5430aa479bde588e69cb9175518d72f9338b6999e3b2ae16fc03d3bdcff8347e486dc031e4ed14601260463c07e1f9a0d7511dfc653712b047c439c680b0b34
+  checksum: 10c0/be88a0b6252f939cb83340bd1b4d450287f752c19271195cd97564fd94047259a9bb8c31c585a61b69d8a1b069a99df9dd804db0132d3359c54d3890c501416a
   languageName: node
   linkType: hard
 
@@ -4325,126 +4465,240 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/eslint-plugin@npm:^8.0.0":
-  version: 8.10.0
-  resolution: "@typescript-eslint/eslint-plugin@npm:8.10.0"
+"@typescript-eslint/eslint-plugin@npm:8.28.0":
+  version: 8.28.0
+  resolution: "@typescript-eslint/eslint-plugin@npm:8.28.0"
   dependencies:
     "@eslint-community/regexpp": "npm:^4.10.0"
-    "@typescript-eslint/scope-manager": "npm:8.10.0"
-    "@typescript-eslint/type-utils": "npm:8.10.0"
-    "@typescript-eslint/utils": "npm:8.10.0"
-    "@typescript-eslint/visitor-keys": "npm:8.10.0"
+    "@typescript-eslint/scope-manager": "npm:8.28.0"
+    "@typescript-eslint/type-utils": "npm:8.28.0"
+    "@typescript-eslint/utils": "npm:8.28.0"
+    "@typescript-eslint/visitor-keys": "npm:8.28.0"
     graphemer: "npm:^1.4.0"
     ignore: "npm:^5.3.1"
     natural-compare: "npm:^1.4.0"
-    ts-api-utils: "npm:^1.3.0"
+    ts-api-utils: "npm:^2.0.1"
   peerDependencies:
     "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0
     eslint: ^8.57.0 || ^9.0.0
-  peerDependenciesMeta:
-    typescript:
-      optional: true
-  checksum: 10c0/4b77ba9c865a2a14e238cd330b5901f0274b8ce1c13324fccd0339b8eea82a50a4709394c903fd8cd5bd0d3aebace0761ff9a4a19fa20b00bb61349b7671c035
+    typescript: ">=4.8.4 <5.9.0"
+  checksum: 10c0/f01b7d231b01ec2c1cc7c40599ddceb329532f2876664a39dec9d25c0aed4cfdbef3ec07f26bac357df000d798f652af6fdb6a2481b6120e43bfa38f7c7a7c48
   languageName: node
   linkType: hard
 
-"@typescript-eslint/parser@npm:^8.0.0":
-  version: 8.10.0
-  resolution: "@typescript-eslint/parser@npm:8.10.0"
+"@typescript-eslint/parser@npm:8.28.0":
+  version: 8.28.0
+  resolution: "@typescript-eslint/parser@npm:8.28.0"
   dependencies:
-    "@typescript-eslint/scope-manager": "npm:8.10.0"
-    "@typescript-eslint/types": "npm:8.10.0"
-    "@typescript-eslint/typescript-estree": "npm:8.10.0"
-    "@typescript-eslint/visitor-keys": "npm:8.10.0"
+    "@typescript-eslint/scope-manager": "npm:8.28.0"
+    "@typescript-eslint/types": "npm:8.28.0"
+    "@typescript-eslint/typescript-estree": "npm:8.28.0"
+    "@typescript-eslint/visitor-keys": "npm:8.28.0"
     debug: "npm:^4.3.4"
   peerDependencies:
     eslint: ^8.57.0 || ^9.0.0
-  peerDependenciesMeta:
-    typescript:
-      optional: true
-  checksum: 10c0/7becb2457c085c239838d301796074b790f46dd38c9fbc14ec1dec8e993c7115cd8a66cdc07983c3a68a2dd92e24e8acc49d69a4ebcc29e9869957eb52d1cb74
+    typescript: ">=4.8.4 <5.9.0"
+  checksum: 10c0/4bde6887bbf3fe031c01e46db90f9f384a8cac2e67c2972b113a62d607db75e01db943601279aac847b9187960a038981814042cb02fd5aa27ea4613028f9313
   languageName: node
   linkType: hard
 
-"@typescript-eslint/scope-manager@npm:8.10.0":
-  version: 8.10.0
-  resolution: "@typescript-eslint/scope-manager@npm:8.10.0"
+"@typescript-eslint/scope-manager@npm:8.28.0":
+  version: 8.28.0
+  resolution: "@typescript-eslint/scope-manager@npm:8.28.0"
   dependencies:
-    "@typescript-eslint/types": "npm:8.10.0"
-    "@typescript-eslint/visitor-keys": "npm:8.10.0"
-  checksum: 10c0/b8bb8635c4d6c00a3578d6265e3ee0f5d96d0c9dee534ed588aa411c3f4497fd71cce730c3ae7571e52453d955b191bc9edcc47c9af21a20c90e9a20f2371108
+    "@typescript-eslint/types": "npm:8.28.0"
+    "@typescript-eslint/visitor-keys": "npm:8.28.0"
+  checksum: 10c0/f3bd76b3f54e60f1efe108b233b2d818e44ecf0dc6422cc296542f784826caf3c66d51b8acc83d8c354980bd201e1d9aa1ea01011de96e0613d320c00e40ccfd
   languageName: node
   linkType: hard
 
-"@typescript-eslint/type-utils@npm:8.10.0":
-  version: 8.10.0
-  resolution: "@typescript-eslint/type-utils@npm:8.10.0"
+"@typescript-eslint/type-utils@npm:8.28.0":
+  version: 8.28.0
+  resolution: "@typescript-eslint/type-utils@npm:8.28.0"
   dependencies:
-    "@typescript-eslint/typescript-estree": "npm:8.10.0"
-    "@typescript-eslint/utils": "npm:8.10.0"
+    "@typescript-eslint/typescript-estree": "npm:8.28.0"
+    "@typescript-eslint/utils": "npm:8.28.0"
     debug: "npm:^4.3.4"
-    ts-api-utils: "npm:^1.3.0"
-  peerDependenciesMeta:
-    typescript:
-      optional: true
-  checksum: 10c0/1af8fce8394279e6ac7bcef449a132072ee36e374c8d557564246ffe7150230844901ca0305e29525bf37c87010e03bf8bedec76fccbfe1e41931cb4f274e208
+    ts-api-utils: "npm:^2.0.1"
+  peerDependencies:
+    eslint: ^8.57.0 || ^9.0.0
+    typescript: ">=4.8.4 <5.9.0"
+  checksum: 10c0/b8936edc2153bf794efba39bfb06393a228217830051767360f4b691fed7c82f3831c4fc6deac6d78b90a58596e61f866c17eaee9dd793c3efda3ebdcf5a71d8
   languageName: node
   linkType: hard
 
-"@typescript-eslint/types@npm:8.10.0":
-  version: 8.10.0
-  resolution: "@typescript-eslint/types@npm:8.10.0"
-  checksum: 10c0/f27dd43c8383e02e914a254257627e393dfc0f08b0f74a253c106813ae361f090271b2f3f2ef588fa3ca1329897d873da595bb5641fe8e3091b25eddca24b5d2
+"@typescript-eslint/types@npm:8.28.0":
+  version: 8.28.0
+  resolution: "@typescript-eslint/types@npm:8.28.0"
+  checksum: 10c0/1f95895e20dac1cf063dc93c99142fd1871e53be816bcbbee93f22a05e6b2a82ca83c20ce3a551f65555910aa0956443a23268edbb004369d0d5cb282d13c377
   languageName: node
   linkType: hard
 
-"@typescript-eslint/typescript-estree@npm:8.10.0":
-  version: 8.10.0
-  resolution: "@typescript-eslint/typescript-estree@npm:8.10.0"
+"@typescript-eslint/typescript-estree@npm:8.28.0":
+  version: 8.28.0
+  resolution: "@typescript-eslint/typescript-estree@npm:8.28.0"
   dependencies:
-    "@typescript-eslint/types": "npm:8.10.0"
-    "@typescript-eslint/visitor-keys": "npm:8.10.0"
+    "@typescript-eslint/types": "npm:8.28.0"
+    "@typescript-eslint/visitor-keys": "npm:8.28.0"
     debug: "npm:^4.3.4"
     fast-glob: "npm:^3.3.2"
     is-glob: "npm:^4.0.3"
     minimatch: "npm:^9.0.4"
     semver: "npm:^7.6.0"
-    ts-api-utils: "npm:^1.3.0"
-  peerDependenciesMeta:
-    typescript:
-      optional: true
-  checksum: 10c0/535a740fe25be0e28fe68c41e3264273d1e5169c9f938e08cc0e3415c357726f43efa44621960108c318fc3305c425d29f3223b6e731d44d67f84058a8947304
+    ts-api-utils: "npm:^2.0.1"
+  peerDependencies:
+    typescript: ">=4.8.4 <5.9.0"
+  checksum: 10c0/97a91c95b1295926098c12e2d2c2abaa68994dc879da132dcce1e75ec9d7dee8187695eaa5241d09cbc42b5e633917b6d35c624e78e3d3ee9bda42d1318080b6
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:8.10.0":
-  version: 8.10.0
-  resolution: "@typescript-eslint/utils@npm:8.10.0"
+"@typescript-eslint/utils@npm:8.28.0, @typescript-eslint/utils@npm:^8.27.0":
+  version: 8.28.0
+  resolution: "@typescript-eslint/utils@npm:8.28.0"
   dependencies:
     "@eslint-community/eslint-utils": "npm:^4.4.0"
-    "@typescript-eslint/scope-manager": "npm:8.10.0"
-    "@typescript-eslint/types": "npm:8.10.0"
-    "@typescript-eslint/typescript-estree": "npm:8.10.0"
+    "@typescript-eslint/scope-manager": "npm:8.28.0"
+    "@typescript-eslint/types": "npm:8.28.0"
+    "@typescript-eslint/typescript-estree": "npm:8.28.0"
   peerDependencies:
     eslint: ^8.57.0 || ^9.0.0
-  checksum: 10c0/a21a2933517176abd00fcd5d8d80023e35dc3d89d5746bbac43790b4e984ab1f371117db08048bce7f42d54c64f4e0e35161149f8f34fd25a27bff9d1110fd16
+    typescript: ">=4.8.4 <5.9.0"
+  checksum: 10c0/d3425be7f86c1245a11f0ea39136af681027797417348d8e666d38c76646945eaed7b35eb8db66372b067dee8b02a855caf2c24c040ec9c31e59681ab223b59d
   languageName: node
   linkType: hard
 
-"@typescript-eslint/visitor-keys@npm:8.10.0":
-  version: 8.10.0
-  resolution: "@typescript-eslint/visitor-keys@npm:8.10.0"
+"@typescript-eslint/visitor-keys@npm:8.28.0":
+  version: 8.28.0
+  resolution: "@typescript-eslint/visitor-keys@npm:8.28.0"
   dependencies:
-    "@typescript-eslint/types": "npm:8.10.0"
-    eslint-visitor-keys: "npm:^3.4.3"
-  checksum: 10c0/14721c4ac939640d5fd1ee1b6eeb07604b11a6017e319e21dcc71e7aac2992341fc7ae1992d977bad4433b6a1d0d1c0c279e6927316b26245f6e333f922fa458
+    "@typescript-eslint/types": "npm:8.28.0"
+    eslint-visitor-keys: "npm:^4.2.0"
+  checksum: 10c0/245a78ed983fe95fbd1b0f2d4cb9e9d1d964bddc0aa3e3d6ab10c19c4273855bfb27d840bb1fd55deb7ae3078b52f26592472baf6fd2c7019a5aa3b1da974f35
   languageName: node
   linkType: hard
 
-"@ungap/structured-clone@npm:^1.2.0":
-  version: 1.2.0
-  resolution: "@ungap/structured-clone@npm:1.2.0"
-  checksum: 10c0/8209c937cb39119f44eb63cf90c0b73e7c754209a6411c707be08e50e29ee81356dca1a848a405c8bdeebfe2f5e4f831ad310ae1689eeef65e7445c090c6657d
+"@unrs/resolver-binding-darwin-arm64@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-darwin-arm64@npm:1.3.2"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-darwin-x64@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-darwin-x64@npm:1.3.2"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-freebsd-x64@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-freebsd-x64@npm:1.3.2"
+  conditions: os=freebsd & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.3.2"
+  conditions: os=linux & cpu=arm
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-linux-arm-musleabihf@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-linux-arm-musleabihf@npm:1.3.2"
+  conditions: os=linux & cpu=arm
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-linux-arm64-gnu@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-linux-arm64-gnu@npm:1.3.2"
+  conditions: os=linux & cpu=arm64 & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-linux-arm64-musl@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-linux-arm64-musl@npm:1.3.2"
+  conditions: os=linux & cpu=arm64 & libc=musl
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-linux-ppc64-gnu@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-linux-ppc64-gnu@npm:1.3.2"
+  conditions: os=linux & cpu=ppc64 & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-linux-s390x-gnu@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-linux-s390x-gnu@npm:1.3.2"
+  conditions: os=linux & cpu=s390x & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-linux-x64-gnu@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-linux-x64-gnu@npm:1.3.2"
+  conditions: os=linux & cpu=x64 & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-linux-x64-musl@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-linux-x64-musl@npm:1.3.2"
+  conditions: os=linux & cpu=x64 & libc=musl
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-wasm32-wasi@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-wasm32-wasi@npm:1.3.2"
+  dependencies:
+    "@napi-rs/wasm-runtime": "npm:^0.2.7"
+  conditions: cpu=wasm32
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-win32-arm64-msvc@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-win32-arm64-msvc@npm:1.3.2"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-win32-ia32-msvc@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-win32-ia32-msvc@npm:1.3.2"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
+"@unrs/resolver-binding-win32-x64-msvc@npm:1.3.2":
+  version: 1.3.2
+  resolution: "@unrs/resolver-binding-win32-x64-msvc@npm:1.3.2"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@use-gesture/core@npm:10.3.1":
+  version: 10.3.1
+  resolution: "@use-gesture/core@npm:10.3.1"
+  checksum: 10c0/2e3b5c0f7fe26cdb47be3a9c2a58a6a9edafc5b2895b07d2898eda9ab5a2b29fb0098b15597baa0856907b593075cd44cc69bba4785c9cfb7b6fabaa3b52cd3e
+  languageName: node
+  linkType: hard
+
+"@use-gesture/react@npm:^10.3.1":
+  version: 10.3.1
+  resolution: "@use-gesture/react@npm:10.3.1"
+  dependencies:
+    "@use-gesture/core": "npm:10.3.1"
+  peerDependencies:
+    react: ">= 16.8.0"
+  checksum: 10c0/978da66e4e7c424866ad52eba8fdf0ce93a4c8fc44f8837c7043e68c6a6107cd67e817fffb27f7db2ae871ef2f6addb0c8ddf1586f24c67b7e6aef1646c668cf
   languageName: node
   linkType: hard
 
@@ -4702,7 +4956,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.12.0, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0":
+"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.14.0, acorn@npm:^8.8.1, acorn@npm:^8.8.2":
+  version: 8.14.1
+  resolution: "acorn@npm:8.14.1"
+  bin:
+    acorn: bin/acorn
+  checksum: 10c0/dbd36c1ed1d2fa3550140000371fcf721578095b18777b85a79df231ca093b08edc6858d75d6e48c73e431c174dcf9214edbd7e6fa5911b93bd8abfa54e47123
+  languageName: node
+  linkType: hard
+
+"acorn@npm:^8.12.0":
   version: 8.12.1
   resolution: "acorn@npm:8.12.1"
   bin:
@@ -4953,13 +5216,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array-buffer-byte-length@npm:^1.0.1":
-  version: 1.0.1
-  resolution: "array-buffer-byte-length@npm:1.0.1"
+"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "array-buffer-byte-length@npm:1.0.2"
   dependencies:
-    call-bind: "npm:^1.0.5"
-    is-array-buffer: "npm:^3.0.4"
-  checksum: 10c0/f5cdf54527cd18a3d2852ddf73df79efec03829e7373a8322ef5df2b4ef546fb365c19c71d6b42d641cb6bfe0f1a2f19bc0ece5b533295f86d7c3d522f228917
+    call-bound: "npm:^1.0.3"
+    is-array-buffer: "npm:^3.0.5"
+  checksum: 10c0/74e1d2d996941c7a1badda9cabb7caab8c449db9086407cad8a1b71d2604cc8abf105db8ca4e02c04579ec58b7be40279ddb09aea4784832984485499f48432d
   languageName: node
   linkType: hard
 
@@ -5061,15 +5324,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array.prototype.flatmap@npm:^1.3.2":
-  version: 1.3.2
-  resolution: "array.prototype.flatmap@npm:1.3.2"
+"array.prototype.flatmap@npm:^1.3.2, array.prototype.flatmap@npm:^1.3.3":
+  version: 1.3.3
+  resolution: "array.prototype.flatmap@npm:1.3.3"
   dependencies:
-    call-bind: "npm:^1.0.2"
-    define-properties: "npm:^1.2.0"
-    es-abstract: "npm:^1.22.1"
-    es-shim-unscopables: "npm:^1.0.0"
-  checksum: 10c0/67b3f1d602bb73713265145853128b1ad77cc0f9b833c7e1e056b323fbeac41a4ff1c9c99c7b9445903caea924d9ca2450578d9011913191aa88cc3c3a4b54f4
+    call-bind: "npm:^1.0.8"
+    define-properties: "npm:^1.2.1"
+    es-abstract: "npm:^1.23.5"
+    es-shim-unscopables: "npm:^1.0.2"
+  checksum: 10c0/ba899ea22b9dc9bf276e773e98ac84638ed5e0236de06f13d63a90b18ca9e0ec7c97d622d899796e3773930b946cd2413d098656c0c5d8cc58c6f25c21e6bd54
   languageName: node
   linkType: hard
 
@@ -5099,19 +5362,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"arraybuffer.prototype.slice@npm:^1.0.3":
-  version: 1.0.3
-  resolution: "arraybuffer.prototype.slice@npm:1.0.3"
+"arraybuffer.prototype.slice@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "arraybuffer.prototype.slice@npm:1.0.4"
   dependencies:
     array-buffer-byte-length: "npm:^1.0.1"
-    call-bind: "npm:^1.0.5"
+    call-bind: "npm:^1.0.8"
     define-properties: "npm:^1.2.1"
-    es-abstract: "npm:^1.22.3"
-    es-errors: "npm:^1.2.1"
-    get-intrinsic: "npm:^1.2.3"
+    es-abstract: "npm:^1.23.5"
+    es-errors: "npm:^1.3.0"
+    get-intrinsic: "npm:^1.2.6"
     is-array-buffer: "npm:^3.0.4"
-    is-shared-array-buffer: "npm:^1.0.2"
-  checksum: 10c0/d32754045bcb2294ade881d45140a5e52bda2321b9e98fa514797b7f0d252c4c5ab0d1edb34112652c62fa6a9398def568da63a4d7544672229afea283358c36
+  checksum: 10c0/2f2459caa06ae0f7f615003f9104b01f6435cc803e11bd2a655107d52a1781dc040532dc44d93026b694cc18793993246237423e13a5337e86b43ed604932c06
   languageName: node
   linkType: hard
 
@@ -5269,13 +5531,13 @@ __metadata:
   linkType: hard
 
 "axios@npm:^1.4.0":
-  version: 1.7.9
-  resolution: "axios@npm:1.7.9"
+  version: 1.8.2
+  resolution: "axios@npm:1.8.2"
   dependencies:
     follow-redirects: "npm:^1.15.6"
     form-data: "npm:^4.0.0"
     proxy-from-env: "npm:^1.1.0"
-  checksum: 10c0/b7a41e24b59fee5f0f26c1fc844b45b17442832eb3a0fb42dd4f1430eb4abc571fe168e67913e8a1d91c993232bd1d1ab03e20e4d1fee8c6147649b576fc1b0b
+  checksum: 10c0/d8c2969e4642dc6d39555ac58effe06c051ba7aac2bd40cad7a9011c019fb2f16ee011c5a6906cb25b8a4f87258c359314eb981f852e60ad445ecaeb793c7aa2
   languageName: node
   linkType: hard
 
@@ -5318,22 +5580,22 @@ __metadata:
   languageName: node
   linkType: hard
 
-"babel-plugin-formatjs@npm:^10.5.1":
-  version: 10.5.34
-  resolution: "babel-plugin-formatjs@npm:10.5.34"
+"babel-plugin-formatjs@npm:^10.5.37":
+  version: 10.5.37
+  resolution: "babel-plugin-formatjs@npm:10.5.37"
   dependencies:
-    "@babel/core": "npm:^7.25.0"
-    "@babel/helper-plugin-utils": "npm:^7.25.0"
-    "@babel/plugin-syntax-jsx": "npm:^7.25.0"
-    "@babel/traverse": "npm:^7.25.0"
-    "@babel/types": "npm:^7.25.0"
-    "@formatjs/icu-messageformat-parser": "npm:2.11.0"
-    "@formatjs/ts-transformer": "npm:3.13.31"
+    "@babel/core": "npm:^7.26.10"
+    "@babel/helper-plugin-utils": "npm:^7.26.5"
+    "@babel/plugin-syntax-jsx": "npm:^7.25.9"
+    "@babel/traverse": "npm:^7.26.10"
+    "@babel/types": "npm:^7.26.10"
+    "@formatjs/icu-messageformat-parser": "npm:2.11.2"
+    "@formatjs/ts-transformer": "npm:3.13.34"
     "@types/babel__core": "npm:^7.20.5"
     "@types/babel__helper-plugin-utils": "npm:^7.10.3"
     "@types/babel__traverse": "npm:^7.20.6"
-    tslib: "npm:2"
-  checksum: 10c0/6e6c67db41044427a7f182e7c18a8519e5aca96645de094244b332cb8c82ed498c56a4634e4e4ed1a4ef85c8e996669ef5bddd63ee9c45d6e512cbbf782af2a2
+    tslib: "npm:^2.8.0"
+  checksum: 10c0/e206ff1a8ad3cbcb3db2d2735d8821701df9d54c8aeb5e8b2861c945af91d4662b9cd37b1ff9d7e17954cdd31aec81788a3d044a1cd9f3e7e8e4f93177097b83
   languageName: node
   linkType: hard
 
@@ -5424,6 +5686,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"babel-plugin-polyfill-corejs3@npm:^0.11.0":
+  version: 0.11.1
+  resolution: "babel-plugin-polyfill-corejs3@npm:0.11.1"
+  dependencies:
+    "@babel/helper-define-polyfill-provider": "npm:^0.6.3"
+    core-js-compat: "npm:^3.40.0"
+  peerDependencies:
+    "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0
+  checksum: 10c0/025f754b6296d84b20200aff63a3c1acdd85e8c621781f2bd27fe2512d0060526192d02329326947c6b29c27cf475fbcfaaff8c51eab1d2bfc7b79086bb64229
+  languageName: node
+  linkType: hard
+
 "babel-plugin-polyfill-regenerator@npm:^0.6.1":
   version: 0.6.1
   resolution: "babel-plugin-polyfill-regenerator@npm:0.6.1"
@@ -5763,17 +6037,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"browserslist@npm:^4.0.0, browserslist@npm:^4.23.0, browserslist@npm:^4.23.1, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0":
-  version: 4.24.0
-  resolution: "browserslist@npm:4.24.0"
+"browserslist@npm:^4.0.0, browserslist@npm:^4.23.0, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3, browserslist@npm:^4.24.4":
+  version: 4.24.4
+  resolution: "browserslist@npm:4.24.4"
   dependencies:
-    caniuse-lite: "npm:^1.0.30001663"
-    electron-to-chromium: "npm:^1.5.28"
-    node-releases: "npm:^2.0.18"
-    update-browserslist-db: "npm:^1.1.0"
+    caniuse-lite: "npm:^1.0.30001688"
+    electron-to-chromium: "npm:^1.5.73"
+    node-releases: "npm:^2.0.19"
+    update-browserslist-db: "npm:^1.1.1"
   bin:
     browserslist: cli.js
-  checksum: 10c0/95e76ad522753c4c470427f6e3c8a4bb5478ff448841e22b3d3e53f89ecaf17b6984666d6c7e715c370f1e7fa0cf684f42e34e554236a8b2fab38ea76b9e4c52
+  checksum: 10c0/db7ebc1733cf471e0b490b4f47e3e2ea2947ce417192c9246644e92c667dd56a71406cc58f62ca7587caf828364892e9952904a02b7aead752bc65b62a37cfe9
   languageName: node
   linkType: hard
 
@@ -5919,16 +6193,35 @@ __metadata:
   languageName: node
   linkType: hard
 
-"call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7":
-  version: 1.0.7
-  resolution: "call-bind@npm:1.0.7"
+"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "call-bind-apply-helpers@npm:1.0.2"
   dependencies:
-    es-define-property: "npm:^1.0.0"
     es-errors: "npm:^1.3.0"
     function-bind: "npm:^1.1.2"
+  checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938
+  languageName: node
+  linkType: hard
+
+"call-bind@npm:^1.0.2, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8":
+  version: 1.0.8
+  resolution: "call-bind@npm:1.0.8"
+  dependencies:
+    call-bind-apply-helpers: "npm:^1.0.0"
+    es-define-property: "npm:^1.0.0"
     get-intrinsic: "npm:^1.2.4"
-    set-function-length: "npm:^1.2.1"
-  checksum: 10c0/a3ded2e423b8e2a265983dba81c27e125b48eefb2655e7dfab6be597088da3d47c47976c24bc51b8fd9af1061f8f87b4ab78a314f3c77784b2ae2ba535ad8b8d
+    set-function-length: "npm:^1.2.2"
+  checksum: 10c0/a13819be0681d915144467741b69875ae5f4eba8961eb0bf322aab63ec87f8250eb6d6b0dcbb2e1349876412a56129ca338592b3829ef4343527f5f18a0752d4
+  languageName: node
+  linkType: hard
+
+"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3":
+  version: 1.0.4
+  resolution: "call-bound@npm:1.0.4"
+  dependencies:
+    call-bind-apply-helpers: "npm:^1.0.2"
+    get-intrinsic: "npm:^1.3.0"
+  checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644
   languageName: node
   linkType: hard
 
@@ -5965,20 +6258,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001599, caniuse-lite@npm:^1.0.30001663":
-  version: 1.0.30001666
-  resolution: "caniuse-lite@npm:1.0.30001666"
-  checksum: 10c0/2d49e9be676233c24717f12aad3d01b3e5f902b457fe1deefaa8d82e64786788a8f79381ae437c61b50e15c9aea8aeb59871b1d54cb4c28b9190d53d292e2339
-  languageName: node
-  linkType: hard
-
-"chalk@npm:4, chalk@npm:^4.0, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0":
-  version: 4.1.2
-  resolution: "chalk@npm:4.1.2"
-  dependencies:
-    ansi-styles: "npm:^4.1.0"
-    supports-color: "npm:^7.1.0"
-  checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
+"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001599, caniuse-lite@npm:^1.0.30001688":
+  version: 1.0.30001699
+  resolution: "caniuse-lite@npm:1.0.30001699"
+  checksum: 10c0/e87b3a0602c3124131f6a21f1eb262378e17a2ee3089e3c472ac8b9caa85cf7d6a219655379302c29c6f10a74051f2a712639d7f98ee0444c73fefcbaf25d519
   languageName: node
   linkType: hard
 
@@ -6003,6 +6286,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"chalk@npm:^4.0, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2":
+  version: 4.1.2
+  resolution: "chalk@npm:4.1.2"
+  dependencies:
+    ansi-styles: "npm:^4.1.0"
+    supports-color: "npm:^7.1.0"
+  checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
+  languageName: node
+  linkType: hard
+
 "chalk@npm:~5.3.0":
   version: 5.3.0
   resolution: "chalk@npm:5.3.0"
@@ -6471,12 +6764,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"core-js-compat@npm:^3.38.0, core-js-compat@npm:^3.38.1":
-  version: 3.38.1
-  resolution: "core-js-compat@npm:3.38.1"
+"core-js-compat@npm:^3.38.0, core-js-compat@npm:^3.40.0":
+  version: 3.40.0
+  resolution: "core-js-compat@npm:3.40.0"
   dependencies:
-    browserslist: "npm:^4.23.3"
-  checksum: 10c0/d8bc8a35591fc5fbf3e376d793f298ec41eb452619c7ef9de4ea59b74be06e9fda799e0dcbf9ba59880dae87e3b41fb191d744ffc988315642a1272bb9442b31
+    browserslist: "npm:^4.24.3"
+  checksum: 10c0/44f6e88726fe266a5be9581a79766800478a8d5c492885f2d4c2a4e2babd9b06bc1689d5340d3a61ae7332f990aff2e83b6203ff8773137a627cfedfbeefabeb
   languageName: node
   linkType: hard
 
@@ -6488,9 +6781,9 @@ __metadata:
   linkType: hard
 
 "core-js@npm:^3.30.2":
-  version: 3.40.0
-  resolution: "core-js@npm:3.40.0"
-  checksum: 10c0/db7946ada881e845d8b157061945b1187618fa45cf162f392a151e8a497962aed2da688c982eaa1d444c864be97a70f8be4d73385294b515d224dd164d19f1d4
+  version: 3.41.0
+  resolution: "core-js@npm:3.41.0"
+  checksum: 10c0/a29ed0b7fe81acf49d04ce5c17a1947166b1c15197327a5d12f95bbe84b46d60c3c13de701d808f41da06fa316285f3f55ce5903abc8d5642afc1eac4457afc8
   languageName: node
   linkType: hard
 
@@ -6620,14 +6913,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
-  version: 7.0.3
-  resolution: "cross-spawn@npm:7.0.3"
+"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6":
+  version: 7.0.6
+  resolution: "cross-spawn@npm:7.0.6"
   dependencies:
     path-key: "npm:^3.1.0"
     shebang-command: "npm:^2.0.0"
     which: "npm:^2.0.1"
-  checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750
+  checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1
   languageName: node
   linkType: hard
 
@@ -7001,36 +7294,36 @@ __metadata:
   languageName: node
   linkType: hard
 
-"data-view-buffer@npm:^1.0.1":
-  version: 1.0.1
-  resolution: "data-view-buffer@npm:1.0.1"
+"data-view-buffer@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "data-view-buffer@npm:1.0.2"
   dependencies:
-    call-bind: "npm:^1.0.6"
+    call-bound: "npm:^1.0.3"
     es-errors: "npm:^1.3.0"
-    is-data-view: "npm:^1.0.1"
-  checksum: 10c0/8984119e59dbed906a11fcfb417d7d861936f16697a0e7216fe2c6c810f6b5e8f4a5281e73f2c28e8e9259027190ac4a33e2a65fdd7fa86ac06b76e838918583
+    is-data-view: "npm:^1.0.2"
+  checksum: 10c0/7986d40fc7979e9e6241f85db8d17060dd9a71bd53c894fa29d126061715e322a4cd47a00b0b8c710394854183d4120462b980b8554012acc1c0fa49df7ad38c
   languageName: node
   linkType: hard
 
-"data-view-byte-length@npm:^1.0.1":
-  version: 1.0.1
-  resolution: "data-view-byte-length@npm:1.0.1"
+"data-view-byte-length@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "data-view-byte-length@npm:1.0.2"
   dependencies:
-    call-bind: "npm:^1.0.7"
+    call-bound: "npm:^1.0.3"
     es-errors: "npm:^1.3.0"
-    is-data-view: "npm:^1.0.1"
-  checksum: 10c0/b7d9e48a0cf5aefed9ab7d123559917b2d7e0d65531f43b2fd95b9d3a6b46042dd3fca597c42bba384e66b70d7ad66ff23932f8367b241f53d93af42cfe04ec2
+    is-data-view: "npm:^1.0.2"
+  checksum: 10c0/f8a4534b5c69384d95ac18137d381f18a5cfae1f0fc1df0ef6feef51ef0d568606d970b69e02ea186c6c0f0eac77fe4e6ad96fec2569cc86c3afcc7475068c55
   languageName: node
   linkType: hard
 
-"data-view-byte-offset@npm:^1.0.0":
-  version: 1.0.0
-  resolution: "data-view-byte-offset@npm:1.0.0"
+"data-view-byte-offset@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "data-view-byte-offset@npm:1.0.1"
   dependencies:
-    call-bind: "npm:^1.0.6"
+    call-bound: "npm:^1.0.2"
     es-errors: "npm:^1.3.0"
     is-data-view: "npm:^1.0.1"
-  checksum: 10c0/21b0d2e53fd6e20cc4257c873bf6d36d77bd6185624b84076c0a1ddaa757b49aaf076254006341d35568e89f52eecd1ccb1a502cfb620f2beca04f48a6a62a8f
+  checksum: 10c0/fa7aa40078025b7810dcffc16df02c480573b7b53ef1205aa6a61533011005c1890e5ba17018c692ce7c900212b547262d33279fde801ad9843edc0863bf78c4
   languageName: node
   linkType: hard
 
@@ -7057,7 +7350,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:~4.4.0":
+"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:~4.4.0":
   version: 4.4.0
   resolution: "debug@npm:4.4.0"
   dependencies:
@@ -7085,10 +7378,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"decimal.js@npm:10, decimal.js@npm:^10.4.2, decimal.js@npm:^10.4.3":
-  version: 10.4.3
-  resolution: "decimal.js@npm:10.4.3"
-  checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee
+"decimal.js@npm:^10.4.2, decimal.js@npm:^10.4.3, decimal.js@npm:^10.5.0":
+  version: 10.5.0
+  resolution: "decimal.js@npm:10.5.0"
+  checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3
   languageName: node
   linkType: hard
 
@@ -7149,7 +7442,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.2, define-data-property@npm:^1.1.4":
+"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4":
   version: 1.1.4
   resolution: "define-data-property@npm:1.1.4"
   dependencies:
@@ -7374,15 +7667,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"doctrine@npm:^3.0.0":
-  version: 3.0.0
-  resolution: "doctrine@npm:3.0.0"
-  dependencies:
-    esutils: "npm:^2.0.2"
-  checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520
-  languageName: node
-  linkType: hard
-
 "dom-accessibility-api@npm:^0.5.9":
   version: 0.5.16
   resolution: "dom-accessibility-api@npm:0.5.16"
@@ -7504,6 +7788,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "dunder-proto@npm:1.0.1"
+  dependencies:
+    call-bind-apply-helpers: "npm:^1.0.1"
+    es-errors: "npm:^1.3.0"
+    gopd: "npm:^1.2.0"
+  checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031
+  languageName: node
+  linkType: hard
+
 "duplexer@npm:^0.1.2":
   version: 0.1.2
   resolution: "duplexer@npm:0.1.2"
@@ -7536,10 +7831,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"electron-to-chromium@npm:^1.5.28":
-  version: 1.5.31
-  resolution: "electron-to-chromium@npm:1.5.31"
-  checksum: 10c0/e8aecd88c4c6d50a9d459b4b222865b855bab8f1b52e82913804e18b7884f2887bd76c61b3aa08c2ccbdcda098dd8486443f75bf770f0138f21dd9e63548fca7
+"electron-to-chromium@npm:^1.5.73":
+  version: 1.5.96
+  resolution: "electron-to-chromium@npm:1.5.96"
+  checksum: 10c0/827d480f35abe8b0d01a4311fc3180089a406edfcd016d8433712b03ec6e56618ad6f9757e35403092de5c2e163372f9ec90eb8e8334f6f26119a21b568d9bf9
   languageName: node
   linkType: hard
 
@@ -7578,7 +7873,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"emoji-regex@npm:10.3.0, emoji-regex@npm:^10.2.1, emoji-regex@npm:^10.3.0":
+"emoji-regex@npm:10.3.0, emoji-regex@npm:^10.3.0":
   version: 10.3.0
   resolution: "emoji-regex@npm:10.3.0"
   checksum: 10c0/b4838e8dcdceb44cf47f59abe352c25ff4fe7857acaf5fb51097c427f6f75b44d052eb907a7a3b86f86bc4eae3a93f5c2b7460abe79c407307e6212d65c91163
@@ -7656,16 +7951,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"enhanced-resolve@npm:^5.15.0":
-  version: 5.17.1
-  resolution: "enhanced-resolve@npm:5.17.1"
-  dependencies:
-    graceful-fs: "npm:^4.2.4"
-    tapable: "npm:^2.2.0"
-  checksum: 10c0/81a0515675eca17efdba2cf5bad87abc91a528fc1191aad50e275e74f045b41506167d420099022da7181c8d787170ea41e4a11a0b10b7a16f6237daecb15370
-  languageName: node
-  linkType: hard
-
 "entities@npm:^2.0.0":
   version: 2.2.0
   resolution: "entities@npm:2.2.0"
@@ -7730,57 +8015,62 @@ __metadata:
   languageName: node
   linkType: hard
 
-"es-abstract@npm:^1.17.2, es-abstract@npm:^1.17.5, es-abstract@npm:^1.20.4, es-abstract@npm:^1.21.2, es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3":
-  version: 1.23.3
-  resolution: "es-abstract@npm:1.23.3"
+"es-abstract@npm:^1.17.2, es-abstract@npm:^1.17.5, es-abstract@npm:^1.20.4, es-abstract@npm:^1.21.2, es-abstract@npm:^1.22.1, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9":
+  version: 1.23.9
+  resolution: "es-abstract@npm:1.23.9"
   dependencies:
-    array-buffer-byte-length: "npm:^1.0.1"
-    arraybuffer.prototype.slice: "npm:^1.0.3"
+    array-buffer-byte-length: "npm:^1.0.2"
+    arraybuffer.prototype.slice: "npm:^1.0.4"
     available-typed-arrays: "npm:^1.0.7"
-    call-bind: "npm:^1.0.7"
-    data-view-buffer: "npm:^1.0.1"
-    data-view-byte-length: "npm:^1.0.1"
-    data-view-byte-offset: "npm:^1.0.0"
-    es-define-property: "npm:^1.0.0"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
+    data-view-buffer: "npm:^1.0.2"
+    data-view-byte-length: "npm:^1.0.2"
+    data-view-byte-offset: "npm:^1.0.1"
+    es-define-property: "npm:^1.0.1"
     es-errors: "npm:^1.3.0"
     es-object-atoms: "npm:^1.0.0"
-    es-set-tostringtag: "npm:^2.0.3"
-    es-to-primitive: "npm:^1.2.1"
-    function.prototype.name: "npm:^1.1.6"
-    get-intrinsic: "npm:^1.2.4"
-    get-symbol-description: "npm:^1.0.2"
-    globalthis: "npm:^1.0.3"
-    gopd: "npm:^1.0.1"
+    es-set-tostringtag: "npm:^2.1.0"
+    es-to-primitive: "npm:^1.3.0"
+    function.prototype.name: "npm:^1.1.8"
+    get-intrinsic: "npm:^1.2.7"
+    get-proto: "npm:^1.0.0"
+    get-symbol-description: "npm:^1.1.0"
+    globalthis: "npm:^1.0.4"
+    gopd: "npm:^1.2.0"
     has-property-descriptors: "npm:^1.0.2"
-    has-proto: "npm:^1.0.3"
-    has-symbols: "npm:^1.0.3"
+    has-proto: "npm:^1.2.0"
+    has-symbols: "npm:^1.1.0"
     hasown: "npm:^2.0.2"
-    internal-slot: "npm:^1.0.7"
-    is-array-buffer: "npm:^3.0.4"
+    internal-slot: "npm:^1.1.0"
+    is-array-buffer: "npm:^3.0.5"
     is-callable: "npm:^1.2.7"
-    is-data-view: "npm:^1.0.1"
-    is-negative-zero: "npm:^2.0.3"
-    is-regex: "npm:^1.1.4"
-    is-shared-array-buffer: "npm:^1.0.3"
-    is-string: "npm:^1.0.7"
-    is-typed-array: "npm:^1.1.13"
-    is-weakref: "npm:^1.0.2"
-    object-inspect: "npm:^1.13.1"
+    is-data-view: "npm:^1.0.2"
+    is-regex: "npm:^1.2.1"
+    is-shared-array-buffer: "npm:^1.0.4"
+    is-string: "npm:^1.1.1"
+    is-typed-array: "npm:^1.1.15"
+    is-weakref: "npm:^1.1.0"
+    math-intrinsics: "npm:^1.1.0"
+    object-inspect: "npm:^1.13.3"
     object-keys: "npm:^1.1.1"
-    object.assign: "npm:^4.1.5"
-    regexp.prototype.flags: "npm:^1.5.2"
-    safe-array-concat: "npm:^1.1.2"
-    safe-regex-test: "npm:^1.0.3"
-    string.prototype.trim: "npm:^1.2.9"
-    string.prototype.trimend: "npm:^1.0.8"
+    object.assign: "npm:^4.1.7"
+    own-keys: "npm:^1.0.1"
+    regexp.prototype.flags: "npm:^1.5.3"
+    safe-array-concat: "npm:^1.1.3"
+    safe-push-apply: "npm:^1.0.0"
+    safe-regex-test: "npm:^1.1.0"
+    set-proto: "npm:^1.0.0"
+    string.prototype.trim: "npm:^1.2.10"
+    string.prototype.trimend: "npm:^1.0.9"
     string.prototype.trimstart: "npm:^1.0.8"
-    typed-array-buffer: "npm:^1.0.2"
-    typed-array-byte-length: "npm:^1.0.1"
-    typed-array-byte-offset: "npm:^1.0.2"
-    typed-array-length: "npm:^1.0.6"
-    unbox-primitive: "npm:^1.0.2"
-    which-typed-array: "npm:^1.1.15"
-  checksum: 10c0/d27e9afafb225c6924bee9971a7f25f20c314f2d6cb93a63cada4ac11dcf42040896a6c22e5fb8f2a10767055ed4ddf400be3b1eb12297d281726de470b75666
+    typed-array-buffer: "npm:^1.0.3"
+    typed-array-byte-length: "npm:^1.0.3"
+    typed-array-byte-offset: "npm:^1.0.4"
+    typed-array-length: "npm:^1.0.7"
+    unbox-primitive: "npm:^1.1.0"
+    which-typed-array: "npm:^1.1.18"
+  checksum: 10c0/1de229c9e08fe13c17fe5abaec8221545dfcd57e51f64909599a6ae896df84b8fd2f7d16c60cb00d7bf495b9298ca3581aded19939d4b7276854a4b066f8422b
   languageName: node
   linkType: hard
 
@@ -7791,41 +8081,41 @@ __metadata:
   languageName: node
   linkType: hard
 
-"es-define-property@npm:^1.0.0":
-  version: 1.0.0
-  resolution: "es-define-property@npm:1.0.0"
-  dependencies:
-    get-intrinsic: "npm:^1.2.4"
-  checksum: 10c0/6bf3191feb7ea2ebda48b577f69bdfac7a2b3c9bcf97307f55fd6ef1bbca0b49f0c219a935aca506c993d8c5d8bddd937766cb760cd5e5a1071351f2df9f9aa4
+"es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "es-define-property@npm:1.0.1"
+  checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c
   languageName: node
   linkType: hard
 
-"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0":
+"es-errors@npm:^1.3.0":
   version: 1.3.0
   resolution: "es-errors@npm:1.3.0"
   checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85
   languageName: node
   linkType: hard
 
-"es-iterator-helpers@npm:^1.0.19, es-iterator-helpers@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "es-iterator-helpers@npm:1.1.0"
+"es-iterator-helpers@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "es-iterator-helpers@npm:1.2.1"
   dependencies:
-    call-bind: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
     define-properties: "npm:^1.2.1"
-    es-abstract: "npm:^1.23.3"
+    es-abstract: "npm:^1.23.6"
     es-errors: "npm:^1.3.0"
     es-set-tostringtag: "npm:^2.0.3"
     function-bind: "npm:^1.1.2"
-    get-intrinsic: "npm:^1.2.4"
+    get-intrinsic: "npm:^1.2.6"
     globalthis: "npm:^1.0.4"
+    gopd: "npm:^1.2.0"
     has-property-descriptors: "npm:^1.0.2"
-    has-proto: "npm:^1.0.3"
-    has-symbols: "npm:^1.0.3"
-    internal-slot: "npm:^1.0.7"
-    iterator.prototype: "npm:^1.1.3"
-    safe-array-concat: "npm:^1.1.2"
-  checksum: 10c0/84d6c240c7da6e62323b336cb1497781546dab16bebdbd879ccfdf588979712d3e941d41165b6c2ffce5a03a7b929d4e6131d3124d330da1a0e2bfa1da7cd99f
+    has-proto: "npm:^1.2.0"
+    has-symbols: "npm:^1.1.0"
+    internal-slot: "npm:^1.1.0"
+    iterator.prototype: "npm:^1.1.4"
+    safe-array-concat: "npm:^1.1.3"
+  checksum: 10c0/97e3125ca472d82d8aceea11b790397648b52c26d8768ea1c1ee6309ef45a8755bb63225a43f3150c7591cffc17caf5752459f1e70d583b4184370a8f04ebd2f
   languageName: node
   linkType: hard
 
@@ -7836,23 +8126,24 @@ __metadata:
   languageName: node
   linkType: hard
 
-"es-object-atoms@npm:^1.0.0":
-  version: 1.0.0
-  resolution: "es-object-atoms@npm:1.0.0"
+"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "es-object-atoms@npm:1.1.1"
   dependencies:
     es-errors: "npm:^1.3.0"
-  checksum: 10c0/1fed3d102eb27ab8d983337bb7c8b159dd2a1e63ff833ec54eea1311c96d5b08223b433060ba240541ca8adba9eee6b0a60cdbf2f80634b784febc9cc8b687b4
+  checksum: 10c0/65364812ca4daf48eb76e2a3b7a89b3f6a2e62a1c420766ce9f692665a29d94fe41fe88b65f24106f449859549711e4b40d9fb8002d862dfd7eb1c512d10be0c
   languageName: node
   linkType: hard
 
-"es-set-tostringtag@npm:^2.0.3":
-  version: 2.0.3
-  resolution: "es-set-tostringtag@npm:2.0.3"
+"es-set-tostringtag@npm:^2.0.3, es-set-tostringtag@npm:^2.1.0":
+  version: 2.1.0
+  resolution: "es-set-tostringtag@npm:2.1.0"
   dependencies:
-    get-intrinsic: "npm:^1.2.4"
+    es-errors: "npm:^1.3.0"
+    get-intrinsic: "npm:^1.2.6"
     has-tostringtag: "npm:^1.0.2"
-    hasown: "npm:^2.0.1"
-  checksum: 10c0/f22aff1585eb33569c326323f0b0d175844a1f11618b86e193b386f8be0ea9474cfbe46df39c45d959f7aa8f6c06985dc51dd6bce5401645ec5a74c4ceaa836a
+    hasown: "npm:^2.0.2"
+  checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af
   languageName: node
   linkType: hard
 
@@ -7865,21 +8156,21 @@ __metadata:
   languageName: node
   linkType: hard
 
-"es-to-primitive@npm:^1.2.1":
-  version: 1.2.1
-  resolution: "es-to-primitive@npm:1.2.1"
+"es-to-primitive@npm:^1.3.0":
+  version: 1.3.0
+  resolution: "es-to-primitive@npm:1.3.0"
   dependencies:
-    is-callable: "npm:^1.1.4"
-    is-date-object: "npm:^1.0.1"
-    is-symbol: "npm:^1.0.2"
-  checksum: 10c0/0886572b8dc075cb10e50c0af62a03d03a68e1e69c388bd4f10c0649ee41b1fbb24840a1b7e590b393011b5cdbe0144b776da316762653685432df37d6de60f1
+    is-callable: "npm:^1.2.7"
+    is-date-object: "npm:^1.0.5"
+    is-symbol: "npm:^1.0.4"
+  checksum: 10c0/c7e87467abb0b438639baa8139f701a06537d2b9bc758f23e8622c3b42fd0fdb5bde0f535686119e446dd9d5e4c0f238af4e14960f4771877cf818d023f6730b
   languageName: node
   linkType: hard
 
-"escalade@npm:^3.1.1, escalade@npm:^3.1.2":
-  version: 3.1.2
-  resolution: "escalade@npm:3.1.2"
-  checksum: 10c0/6b4adafecd0682f3aa1cd1106b8fff30e492c7015b178bc81b2d2f75106dabea6c6d6e8508fc491bd58e597c74abb0e8e2368f943ecb9393d4162e3c2f3cf287
+"escalade@npm:^3.1.1, escalade@npm:^3.2.0":
+  version: 3.2.0
+  resolution: "escalade@npm:3.2.0"
+  checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65
   languageName: node
   linkType: hard
 
@@ -7929,13 +8220,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"eslint-define-config@npm:^2.0.0":
-  version: 2.1.0
-  resolution: "eslint-define-config@npm:2.1.0"
-  checksum: 10c0/034bd6bfbfec2db6c720a51815de6b072efeef7afbf99d90c23a1871f9cd741bb77f9d34e0bc2465262298c6110c5c45b704714d8575c6567fd2df963fb792ea
-  languageName: node
-  linkType: hard
-
 "eslint-import-resolver-node@npm:^0.3.9":
   version: 0.3.9
   resolution: "eslint-import-resolver-node@npm:0.3.9"
@@ -7947,18 +8231,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"eslint-import-resolver-typescript@npm:^3.5.5":
-  version: 3.6.3
-  resolution: "eslint-import-resolver-typescript@npm:3.6.3"
+"eslint-import-resolver-typescript@npm:^4.2.5":
+  version: 4.2.5
+  resolution: "eslint-import-resolver-typescript@npm:4.2.5"
   dependencies:
-    "@nolyfill/is-core-module": "npm:1.0.39"
-    debug: "npm:^4.3.5"
-    enhanced-resolve: "npm:^5.15.0"
-    eslint-module-utils: "npm:^2.8.1"
-    fast-glob: "npm:^3.3.2"
-    get-tsconfig: "npm:^4.7.5"
-    is-bun-module: "npm:^1.0.2"
-    is-glob: "npm:^4.0.3"
+    debug: "npm:^4.4.0"
+    get-tsconfig: "npm:^4.10.0"
+    is-bun-module: "npm:^2.0.0"
+    stable-hash: "npm:^0.0.5"
+    tinyglobby: "npm:^0.2.12"
+    unrs-resolver: "npm:^1.3.2"
   peerDependencies:
     eslint: "*"
     eslint-plugin-import: "*"
@@ -7968,46 +8250,44 @@ __metadata:
       optional: true
     eslint-plugin-import-x:
       optional: true
-  checksum: 10c0/5933b00791b7b077725b9ba9a85327d2e2dc7c8944c18a868feb317a0bf0e1e77aed2254c9c5e24dcc49360d119331d2c15281837f4269592965ace380a75111
+  checksum: 10c0/9134c4dd6e8b3cf1356d6bff68939153c81255c0ac7f694e829c3c7f5e785936591cfe43209d866c8a3b379d3a8dcd203651ec49bd99361fcb54dc0c2b9ce8fc
   languageName: node
   linkType: hard
 
-"eslint-module-utils@npm:^2.8.1, eslint-module-utils@npm:^2.9.0":
-  version: 2.9.0
-  resolution: "eslint-module-utils@npm:2.9.0"
+"eslint-module-utils@npm:^2.12.0":
+  version: 2.12.0
+  resolution: "eslint-module-utils@npm:2.12.0"
   dependencies:
     debug: "npm:^3.2.7"
   peerDependenciesMeta:
     eslint:
       optional: true
-  checksum: 10c0/7c45c5b54402a969e99315890c10e9bf8c8bee16c7890573343af05dfa04566d61546585678c413e5228af0550e39461be47e35a8ff0d1863e113bdbb28d1d29
+  checksum: 10c0/4d8b46dcd525d71276f9be9ffac1d2be61c9d54cc53c992e6333cf957840dee09381842b1acbbb15fc6b255ebab99cd481c5007ab438e5455a14abe1a0468558
   languageName: node
   linkType: hard
 
-"eslint-plugin-formatjs@npm:^5.0.0":
-  version: 5.1.1
-  resolution: "eslint-plugin-formatjs@npm:5.1.1"
+"eslint-plugin-formatjs@npm:^5.3.1":
+  version: 5.3.1
+  resolution: "eslint-plugin-formatjs@npm:5.3.1"
   dependencies:
-    "@formatjs/icu-messageformat-parser": "npm:2.7.10"
-    "@formatjs/ts-transformer": "npm:3.13.16"
-    "@types/eslint": "npm:9"
-    "@types/picomatch": "npm:^2.3.0"
-    "@typescript-eslint/utils": "npm:8.10.0"
-    emoji-regex: "npm:^10.2.1"
+    "@formatjs/icu-messageformat-parser": "npm:2.11.2"
+    "@formatjs/ts-transformer": "npm:3.13.34"
+    "@types/eslint": "npm:^9.6.1"
+    "@types/picomatch": "npm:^3"
+    "@typescript-eslint/utils": "npm:^8.27.0"
     magic-string: "npm:^0.30.0"
-    picomatch: "npm:^2.3.1"
-    tslib: "npm:^2.7.0"
-    typescript: "npm:5"
+    picomatch: "npm:2 || 3 || 4"
+    tslib: "npm:^2.8.0"
     unicode-emoji-utils: "npm:^1.2.0"
   peerDependencies:
-    eslint: 9
-  checksum: 10c0/b0cf41208fe1bdcd4e7879c0a7099273753ec1e2c5a855c0989679690a740b92123d1625cea7ce862a19412a588fd16e42c0c196807021f13462585ace719dc1
+    eslint: ^9.23.0
+  checksum: 10c0/fb8ba06e0718cd098f2393aea04eb4a6037ca3ead1c9450bd38926d0adecba4cefdebfb661c56c36685e0f003331520c3330544c45803f397b827713ab5e1d7d
   languageName: node
   linkType: hard
 
-"eslint-plugin-import@npm:~2.30.0":
-  version: 2.30.0
-  resolution: "eslint-plugin-import@npm:2.30.0"
+"eslint-plugin-import@npm:~2.31.0":
+  version: 2.31.0
+  resolution: "eslint-plugin-import@npm:2.31.0"
   dependencies:
     "@rtsao/scc": "npm:^1.1.0"
     array-includes: "npm:^3.1.8"
@@ -8017,7 +8297,7 @@ __metadata:
     debug: "npm:^3.2.7"
     doctrine: "npm:^2.1.0"
     eslint-import-resolver-node: "npm:^0.3.9"
-    eslint-module-utils: "npm:^2.9.0"
+    eslint-module-utils: "npm:^2.12.0"
     hasown: "npm:^2.0.2"
     is-core-module: "npm:^2.15.1"
     is-glob: "npm:^4.0.3"
@@ -8026,16 +8306,17 @@ __metadata:
     object.groupby: "npm:^1.0.3"
     object.values: "npm:^1.2.0"
     semver: "npm:^6.3.1"
+    string.prototype.trimend: "npm:^1.0.8"
     tsconfig-paths: "npm:^3.15.0"
   peerDependencies:
-    eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
-  checksum: 10c0/4c9dcb1f27505c4d5dd891d2b551f56c70786d136aa3992a77e785bdc67c9f60200a2c7fb0ce55b7647fe550b12bc433d5dfa59e2c00ab44227791c5ab86badf
+    eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
+  checksum: 10c0/e21d116ddd1900e091ad120b3eb68c5dd5437fe2c930f1211781cd38b246f090a6b74d5f3800b8255a0ed29782591521ad44eb21c5534960a8f1fb4040fd913a
   languageName: node
   linkType: hard
 
-"eslint-plugin-jsdoc@npm:^50.0.0":
-  version: 50.4.3
-  resolution: "eslint-plugin-jsdoc@npm:50.4.3"
+"eslint-plugin-jsdoc@npm:^50.6.9":
+  version: 50.6.9
+  resolution: "eslint-plugin-jsdoc@npm:50.6.9"
   dependencies:
     "@es-joy/jsdoccomment": "npm:~0.49.0"
     are-docs-informative: "npm:^0.0.2"
@@ -8050,13 +8331,13 @@ __metadata:
     synckit: "npm:^0.9.1"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
-  checksum: 10c0/96067f8fc3553371e9afdc6d03c166228bfd3cb004fcd70c4357d49185732f384351e20f44c21b0a13ea318c8aabbd584b627804f62a2a80376507646708a786
+  checksum: 10c0/cad199d262c2e889a3af4e402f6adc624e4273b3d5ca1940e7227b37d87af8090ca3444f7fff57f58dab9a827faed8722fc2f5d4daf31ec085eb00e9f5a338a7
   languageName: node
   linkType: hard
 
-"eslint-plugin-jsx-a11y@npm:~6.10.0":
-  version: 6.10.1
-  resolution: "eslint-plugin-jsx-a11y@npm:6.10.1"
+"eslint-plugin-jsx-a11y@npm:~6.10.2":
+  version: 6.10.2
+  resolution: "eslint-plugin-jsx-a11y@npm:6.10.2"
   dependencies:
     aria-query: "npm:^5.3.2"
     array-includes: "npm:^3.1.8"
@@ -8066,7 +8347,6 @@ __metadata:
     axobject-query: "npm:^4.1.0"
     damerau-levenshtein: "npm:^1.0.8"
     emoji-regex: "npm:^9.2.2"
-    es-iterator-helpers: "npm:^1.1.0"
     hasown: "npm:^2.0.2"
     jsx-ast-utils: "npm:^3.3.5"
     language-tags: "npm:^1.0.9"
@@ -8076,53 +8356,55 @@ __metadata:
     string.prototype.includes: "npm:^2.0.1"
   peerDependencies:
     eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
-  checksum: 10c0/25bf28e3db4f6789c5d4f9300fc6fc54faca19ecc537d0f46e9c873f80ed37103a033e1f716f608fab5f5813dd38af65a9a6ae2e29dd079763ce539ebecf998e
+  checksum: 10c0/d93354e03b0cf66f018d5c50964e074dffe4ddf1f9b535fa020d19c4ae45f89c1a16e9391ca61ac3b19f7042c751ac0d361a056a65cbd1de24718a53ff8daa6e
   languageName: node
   linkType: hard
 
-"eslint-plugin-promise@npm:~7.1.0":
-  version: 7.1.0
-  resolution: "eslint-plugin-promise@npm:7.1.0"
+"eslint-plugin-promise@npm:~7.2.1":
+  version: 7.2.1
+  resolution: "eslint-plugin-promise@npm:7.2.1"
+  dependencies:
+    "@eslint-community/eslint-utils": "npm:^4.4.0"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
-  checksum: 10c0/bbc3406139715dfa5f48d04f6d5b5e82f68929d954b0fa3821eb8cd6dc381b210512cedd2d874e5de5381005d316566f4ae046a4750ce3f5f5cbf28a14cc0ab2
+  checksum: 10c0/d494982faeeafbd2aa5fae9cbceca546169a8399000f72d5d940fa5c4ba554612903bcafbb8033647179e5d21ccf1d621b433d089695f7f47ce3d9fcf4cd0abf
   languageName: node
   linkType: hard
 
-"eslint-plugin-react-hooks@npm:^5.0.0":
-  version: 5.0.0
-  resolution: "eslint-plugin-react-hooks@npm:5.0.0"
+"eslint-plugin-react-hooks@npm:^5.2.0":
+  version: 5.2.0
+  resolution: "eslint-plugin-react-hooks@npm:5.2.0"
   peerDependencies:
     eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
-  checksum: 10c0/bcb74b421f32e4203a7100405b57aab85526be4461e5a1da01bc537969a30012d2ee209a2c2a6cac543833a27188ce1e6ad71e4628d0bb4a2e5365cad86c5002
+  checksum: 10c0/1c8d50fa5984c6dea32470651807d2922cc3934cf3425e78f84a24c2dfd972e7f019bee84aefb27e0cf2c13fea0ac1d4473267727408feeb1c56333ca1489385
   languageName: node
   linkType: hard
 
-"eslint-plugin-react@npm:^7.33.2":
-  version: 7.37.1
-  resolution: "eslint-plugin-react@npm:7.37.1"
+"eslint-plugin-react@npm:^7.37.4":
+  version: 7.37.4
+  resolution: "eslint-plugin-react@npm:7.37.4"
   dependencies:
     array-includes: "npm:^3.1.8"
     array.prototype.findlast: "npm:^1.2.5"
-    array.prototype.flatmap: "npm:^1.3.2"
+    array.prototype.flatmap: "npm:^1.3.3"
     array.prototype.tosorted: "npm:^1.1.4"
     doctrine: "npm:^2.1.0"
-    es-iterator-helpers: "npm:^1.0.19"
+    es-iterator-helpers: "npm:^1.2.1"
     estraverse: "npm:^5.3.0"
     hasown: "npm:^2.0.2"
     jsx-ast-utils: "npm:^2.4.1 || ^3.0.0"
     minimatch: "npm:^3.1.2"
     object.entries: "npm:^1.1.8"
     object.fromentries: "npm:^2.0.8"
-    object.values: "npm:^1.2.0"
+    object.values: "npm:^1.2.1"
     prop-types: "npm:^15.8.1"
     resolve: "npm:^2.0.0-next.5"
     semver: "npm:^6.3.1"
-    string.prototype.matchall: "npm:^4.0.11"
+    string.prototype.matchall: "npm:^4.0.12"
     string.prototype.repeat: "npm:^1.0.0"
   peerDependencies:
     eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
-  checksum: 10c0/13cf55666f16d2ca45b14aad1b0e14741d1817679c86d20aff0bc1e802439a8541f40a42c4c8e3486ffb710f1bcc2f3e56697f2b5f724306a7fca174e1ad6433
+  checksum: 10c0/4acbbdb19669dfa9a162ed8847c3ad1918f6aea1ceb675ee320b5d903b4e463fdef25e15233295b6d0a726fef2ea8b015c527da769c7690932ddc52d5b82ba12
   languageName: node
   linkType: hard
 
@@ -8136,75 +8418,88 @@ __metadata:
   languageName: node
   linkType: hard
 
-"eslint-scope@npm:^7.2.2":
-  version: 7.2.2
-  resolution: "eslint-scope@npm:7.2.2"
+"eslint-scope@npm:^8.3.0":
+  version: 8.3.0
+  resolution: "eslint-scope@npm:8.3.0"
   dependencies:
     esrecurse: "npm:^4.3.0"
     estraverse: "npm:^5.2.0"
-  checksum: 10c0/613c267aea34b5a6d6c00514e8545ef1f1433108097e857225fed40d397dd6b1809dffd11c2fde23b37ca53d7bf935fe04d2a18e6fc932b31837b6ad67e1c116
+  checksum: 10c0/23bf54345573201fdf06d29efa345ab508b355492f6c6cc9e2b9f6d02b896f369b6dd5315205be94b8853809776c4d13353b85c6b531997b164ff6c3328ecf5b
   languageName: node
   linkType: hard
 
-"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3":
+"eslint-visitor-keys@npm:^3.3.0":
   version: 3.4.3
   resolution: "eslint-visitor-keys@npm:3.4.3"
   checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820
   languageName: node
   linkType: hard
 
-"eslint-visitor-keys@npm:^4.0.0":
-  version: 4.0.0
-  resolution: "eslint-visitor-keys@npm:4.0.0"
-  checksum: 10c0/76619f42cf162705a1515a6868e6fc7567e185c7063a05621a8ac4c3b850d022661262c21d9f1fc1d144ecf0d5d64d70a3f43c15c3fc969a61ace0fb25698cf5
+"eslint-visitor-keys@npm:^4.0.0, eslint-visitor-keys@npm:^4.2.0":
+  version: 4.2.0
+  resolution: "eslint-visitor-keys@npm:4.2.0"
+  checksum: 10c0/2ed81c663b147ca6f578312919483eb040295bbab759e5a371953456c636c5b49a559883e2677112453728d66293c0a4c90ab11cab3428cf02a0236d2e738269
   languageName: node
   linkType: hard
 
-"eslint@npm:^8.41.0":
-  version: 8.57.1
-  resolution: "eslint@npm:8.57.1"
+"eslint@npm:^9.23.0":
+  version: 9.23.0
+  resolution: "eslint@npm:9.23.0"
   dependencies:
     "@eslint-community/eslint-utils": "npm:^4.2.0"
-    "@eslint-community/regexpp": "npm:^4.6.1"
-    "@eslint/eslintrc": "npm:^2.1.4"
-    "@eslint/js": "npm:8.57.1"
-    "@humanwhocodes/config-array": "npm:^0.13.0"
+    "@eslint-community/regexpp": "npm:^4.12.1"
+    "@eslint/config-array": "npm:^0.19.2"
+    "@eslint/config-helpers": "npm:^0.2.0"
+    "@eslint/core": "npm:^0.12.0"
+    "@eslint/eslintrc": "npm:^3.3.1"
+    "@eslint/js": "npm:9.23.0"
+    "@eslint/plugin-kit": "npm:^0.2.7"
+    "@humanfs/node": "npm:^0.16.6"
     "@humanwhocodes/module-importer": "npm:^1.0.1"
-    "@nodelib/fs.walk": "npm:^1.2.8"
-    "@ungap/structured-clone": "npm:^1.2.0"
+    "@humanwhocodes/retry": "npm:^0.4.2"
+    "@types/estree": "npm:^1.0.6"
+    "@types/json-schema": "npm:^7.0.15"
     ajv: "npm:^6.12.4"
     chalk: "npm:^4.0.0"
-    cross-spawn: "npm:^7.0.2"
+    cross-spawn: "npm:^7.0.6"
     debug: "npm:^4.3.2"
-    doctrine: "npm:^3.0.0"
     escape-string-regexp: "npm:^4.0.0"
-    eslint-scope: "npm:^7.2.2"
-    eslint-visitor-keys: "npm:^3.4.3"
-    espree: "npm:^9.6.1"
-    esquery: "npm:^1.4.2"
+    eslint-scope: "npm:^8.3.0"
+    eslint-visitor-keys: "npm:^4.2.0"
+    espree: "npm:^10.3.0"
+    esquery: "npm:^1.5.0"
     esutils: "npm:^2.0.2"
     fast-deep-equal: "npm:^3.1.3"
-    file-entry-cache: "npm:^6.0.1"
+    file-entry-cache: "npm:^8.0.0"
     find-up: "npm:^5.0.0"
     glob-parent: "npm:^6.0.2"
-    globals: "npm:^13.19.0"
-    graphemer: "npm:^1.4.0"
     ignore: "npm:^5.2.0"
     imurmurhash: "npm:^0.1.4"
     is-glob: "npm:^4.0.0"
-    is-path-inside: "npm:^3.0.3"
-    js-yaml: "npm:^4.1.0"
     json-stable-stringify-without-jsonify: "npm:^1.0.1"
-    levn: "npm:^0.4.1"
     lodash.merge: "npm:^4.6.2"
     minimatch: "npm:^3.1.2"
     natural-compare: "npm:^1.4.0"
     optionator: "npm:^0.9.3"
-    strip-ansi: "npm:^6.0.1"
-    text-table: "npm:^0.2.0"
+  peerDependencies:
+    jiti: "*"
+  peerDependenciesMeta:
+    jiti:
+      optional: true
   bin:
     eslint: bin/eslint.js
-  checksum: 10c0/1fd31533086c1b72f86770a4d9d7058ee8b4643fd1cfd10c7aac1ecb8725698e88352a87805cf4b2ce890aa35947df4b4da9655fb7fdfa60dbb448a43f6ebcf1
+  checksum: 10c0/9616c308dfa8d09db8ae51019c87d5d05933742214531b077bd6ab618baab3bec7938256c14dcad4dc47f5ba93feb0bc5e089f68799f076374ddea21b6a9be45
+  languageName: node
+  linkType: hard
+
+"espree@npm:^10.0.1, espree@npm:^10.3.0":
+  version: 10.3.0
+  resolution: "espree@npm:10.3.0"
+  dependencies:
+    acorn: "npm:^8.14.0"
+    acorn-jsx: "npm:^5.3.2"
+    eslint-visitor-keys: "npm:^4.2.0"
+  checksum: 10c0/272beeaca70d0a1a047d61baff64db04664a33d7cfb5d144f84bc8a5c6194c6c8ebe9cc594093ca53add88baa23e59b01e69e8a0160ab32eac570482e165c462
   languageName: node
   linkType: hard
 
@@ -8219,17 +8514,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"espree@npm:^9.6.0, espree@npm:^9.6.1":
-  version: 9.6.1
-  resolution: "espree@npm:9.6.1"
-  dependencies:
-    acorn: "npm:^8.9.0"
-    acorn-jsx: "npm:^5.3.2"
-    eslint-visitor-keys: "npm:^3.4.1"
-  checksum: 10c0/1a2e9b4699b715347f62330bcc76aee224390c28bb02b31a3752e9d07549c473f5f986720483c6469cf3cfb3c9d05df612ffc69eb1ee94b54b739e67de9bb460
-  languageName: node
-  linkType: hard
-
 "esprima@npm:^4.0.0, esprima@npm:^4.0.1":
   version: 4.0.1
   resolution: "esprima@npm:4.0.1"
@@ -8240,7 +8524,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"esquery@npm:^1.4.2, esquery@npm:^1.6.0":
+"esquery@npm:^1.5.0, esquery@npm:^1.6.0":
   version: 1.6.0
   resolution: "esquery@npm:1.6.0"
   dependencies:
@@ -8602,12 +8886,24 @@ __metadata:
   languageName: node
   linkType: hard
 
-"file-entry-cache@npm:^6.0.1":
-  version: 6.0.1
-  resolution: "file-entry-cache@npm:6.0.1"
+"fdir@npm:^6.4.3":
+  version: 6.4.3
+  resolution: "fdir@npm:6.4.3"
+  peerDependencies:
+    picomatch: ^3 || ^4
+  peerDependenciesMeta:
+    picomatch:
+      optional: true
+  checksum: 10c0/d13c10120e9625adf21d8d80481586200759928c19405a816b77dd28eaeb80e7c59c5def3e2941508045eb06d34eb47fad865ccc8bf98e6ab988bb0ed160fb6f
+  languageName: node
+  linkType: hard
+
+"file-entry-cache@npm:^8.0.0":
+  version: 8.0.0
+  resolution: "file-entry-cache@npm:8.0.0"
   dependencies:
-    flat-cache: "npm:^3.0.4"
-  checksum: 10c0/58473e8a82794d01b38e5e435f6feaf648e3f36fdb3a56e98f417f4efae71ad1c0d4ebd8a9a7c50c3ad085820a93fc7494ad721e0e4ebc1da3573f4e1c3c7cdd
+    flat-cache: "npm:^4.0.0"
+  checksum: 10c0/9e2b5938b1cd9b6d7e3612bdc533afd4ac17b2fc646569e9a8abbf2eb48e5eb8e316bc38815a3ef6a1b456f4107f0d0f055a614ca613e75db6bf9ff4d72c1638
   languageName: node
   linkType: hard
 
@@ -8743,14 +9039,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"flat-cache@npm:^3.0.4":
-  version: 3.2.0
-  resolution: "flat-cache@npm:3.2.0"
+"flat-cache@npm:^4.0.0":
+  version: 4.0.1
+  resolution: "flat-cache@npm:4.0.1"
   dependencies:
     flatted: "npm:^3.2.9"
-    keyv: "npm:^4.5.3"
-    rimraf: "npm:^3.0.2"
-  checksum: 10c0/b76f611bd5f5d68f7ae632e3ae503e678d205cf97a17c6ab5b12f6ca61188b5f1f7464503efae6dc18683ed8f0b41460beb48ac4b9ac63fe6201296a91ba2f75
+    keyv: "npm:^4.5.4"
+  checksum: 10c0/2c59d93e9faa2523e4fda6b4ada749bed432cfa28c8e251f33b25795e426a1c6dbada777afb1f74fcfff33934fdbdea921ee738fcc33e71adc9d6eca984a1cfc
   languageName: node
   linkType: hard
 
@@ -8816,7 +9111,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"form-data@npm:^4.0.0, form-data@npm:^4.0.1":
+"form-data@npm:^4.0.0":
   version: 4.0.1
   resolution: "form-data@npm:4.0.1"
   dependencies:
@@ -8941,15 +9236,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"function.prototype.name@npm:^1.1.5, function.prototype.name@npm:^1.1.6":
-  version: 1.1.6
-  resolution: "function.prototype.name@npm:1.1.6"
+"function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8":
+  version: 1.1.8
+  resolution: "function.prototype.name@npm:1.1.8"
   dependencies:
-    call-bind: "npm:^1.0.2"
-    define-properties: "npm:^1.2.0"
-    es-abstract: "npm:^1.22.1"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
+    define-properties: "npm:^1.2.1"
     functions-have-names: "npm:^1.2.3"
-  checksum: 10c0/9eae11294905b62cb16874adb4fc687927cda3162285e0ad9612e6a1d04934005d46907362ea9cdb7428edce05a2f2c3dabc3b2d21e9fd343e9bb278230ad94b
+    hasown: "npm:^2.0.2"
+    is-callable: "npm:^1.2.7"
+  checksum: 10c0/e920a2ab52663005f3cbe7ee3373e3c71c1fb5558b0b0548648cdf3e51961085032458e26c71ff1a8c8c20e7ee7caeb03d43a5d1fa8610c459333323a2e71253
   languageName: node
   linkType: hard
 
@@ -8988,16 +9285,21 @@ __metadata:
   languageName: node
   linkType: hard
 
-"get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4":
-  version: 1.2.4
-  resolution: "get-intrinsic@npm:1.2.4"
+"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0":
+  version: 1.3.0
+  resolution: "get-intrinsic@npm:1.3.0"
   dependencies:
+    call-bind-apply-helpers: "npm:^1.0.2"
+    es-define-property: "npm:^1.0.1"
     es-errors: "npm:^1.3.0"
+    es-object-atoms: "npm:^1.1.1"
     function-bind: "npm:^1.1.2"
-    has-proto: "npm:^1.0.1"
-    has-symbols: "npm:^1.0.3"
-    hasown: "npm:^2.0.0"
-  checksum: 10c0/0a9b82c16696ed6da5e39b1267104475c47e3a9bdbe8b509dfe1710946e38a87be70d759f4bb3cda042d76a41ef47fe769660f3b7c0d1f68750299344ffb15b7
+    get-proto: "npm:^1.0.1"
+    gopd: "npm:^1.2.0"
+    has-symbols: "npm:^1.1.0"
+    hasown: "npm:^2.0.2"
+    math-intrinsics: "npm:^1.1.0"
+  checksum: 10c0/52c81808af9a8130f581e6a6a83e1ba4a9f703359e7a438d1369a5267a25412322f03dcbd7c549edaef0b6214a0630a28511d7df0130c93cfd380f4fa0b5b66a
   languageName: node
   linkType: hard
 
@@ -9015,6 +9317,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"get-proto@npm:^1.0.0, get-proto@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "get-proto@npm:1.0.1"
+  dependencies:
+    dunder-proto: "npm:^1.0.1"
+    es-object-atoms: "npm:^1.0.0"
+  checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c
+  languageName: node
+  linkType: hard
+
 "get-stream@npm:^4.0.0":
   version: 4.1.0
   resolution: "get-stream@npm:4.1.0"
@@ -9038,23 +9350,23 @@ __metadata:
   languageName: node
   linkType: hard
 
-"get-symbol-description@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "get-symbol-description@npm:1.0.2"
+"get-symbol-description@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "get-symbol-description@npm:1.1.0"
   dependencies:
-    call-bind: "npm:^1.0.5"
+    call-bound: "npm:^1.0.3"
     es-errors: "npm:^1.3.0"
-    get-intrinsic: "npm:^1.2.4"
-  checksum: 10c0/867be6d63f5e0eb026cb3b0ef695ec9ecf9310febb041072d2e142f260bd91ced9eeb426b3af98791d1064e324e653424afa6fd1af17dee373bea48ae03162bc
+    get-intrinsic: "npm:^1.2.6"
+  checksum: 10c0/d6a7d6afca375779a4b307738c9e80dbf7afc0bdbe5948768d54ab9653c865523d8920e670991a925936eb524b7cb6a6361d199a760b21d0ca7620194455aa4b
   languageName: node
   linkType: hard
 
-"get-tsconfig@npm:^4.7.5":
-  version: 4.8.0
-  resolution: "get-tsconfig@npm:4.8.0"
+"get-tsconfig@npm:^4.10.0":
+  version: 4.10.0
+  resolution: "get-tsconfig@npm:4.10.0"
   dependencies:
     resolve-pkg-maps: "npm:^1.0.0"
-  checksum: 10c0/943721c996d9a77351aa7c07956de77baece97f997bd30f3247f46907e4b743f7b9da02c7b3692a36f0884d3724271faeb88ed1c3aca3aba2afe3f27d6c4aeb3
+  checksum: 10c0/c9b5572c5118923c491c04285c73bd55b19e214992af957c502a3be0fc0043bb421386ffd45ca3433c0a7fba81221ca300479e8393960acf15d0ed4563f38a86
   languageName: node
   linkType: hard
 
@@ -9174,16 +9486,21 @@ __metadata:
   languageName: node
   linkType: hard
 
-"globals@npm:^13.19.0":
-  version: 13.23.0
-  resolution: "globals@npm:13.23.0"
-  dependencies:
-    type-fest: "npm:^0.20.2"
-  checksum: 10c0/fc05e184b3be59bffa2580f28551a12a758c3a18df4be91444202982c76f13f52821ad54ffaf7d3f2a4d2498fdf54aeaca8d4540fd9e860a9edb09d34ef4c507
+"globals@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "globals@npm:14.0.0"
+  checksum: 10c0/b96ff42620c9231ad468d4c58ff42afee7777ee1c963013ff8aabe095a451d0ceeb8dcd8ef4cbd64d2538cef45f787a78ba3a9574f4a634438963e334471302d
   languageName: node
   linkType: hard
 
-"globalthis@npm:^1.0.3, globalthis@npm:^1.0.4":
+"globals@npm:^16.0.0":
+  version: 16.0.0
+  resolution: "globals@npm:16.0.0"
+  checksum: 10c0/8906d5f01838df64a81d6c2a7b7214312e2216cf65c5ed1546dc9a7d0febddf55ffa906cf04efd5b01eec2534d6f14859a89535d1a68241832810e41ef3fd5bb
+  languageName: node
+  linkType: hard
+
+"globalthis@npm:^1.0.4":
   version: 1.0.4
   resolution: "globalthis@npm:1.0.4"
   dependencies:
@@ -9227,16 +9544,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"gopd@npm:^1.0.1":
-  version: 1.0.1
-  resolution: "gopd@npm:1.0.1"
-  dependencies:
-    get-intrinsic: "npm:^1.1.3"
-  checksum: 10c0/505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63
+"gopd@npm:^1.0.1, gopd@npm:^1.2.0":
+  version: 1.2.0
+  resolution: "gopd@npm:1.2.0"
+  checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead
   languageName: node
   linkType: hard
 
-"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9":
+"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9":
   version: 4.2.11
   resolution: "graceful-fs@npm:4.2.11"
   checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
@@ -9266,7 +9581,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2":
+"has-bigints@npm:^1.0.2":
   version: 1.0.2
   resolution: "has-bigints@npm:1.0.2"
   checksum: 10c0/724eb1485bfa3cdff6f18d95130aa190561f00b3fcf9f19dc640baf8176b5917c143b81ec2123f8cddb6c05164a198c94b13e1377c497705ccc8e1a80306e83b
@@ -9287,7 +9602,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.1, has-property-descriptors@npm:^1.0.2":
+"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2":
   version: 1.0.2
   resolution: "has-property-descriptors@npm:1.0.2"
   dependencies:
@@ -9296,17 +9611,19 @@ __metadata:
   languageName: node
   linkType: hard
 
-"has-proto@npm:^1.0.1, has-proto@npm:^1.0.3":
-  version: 1.0.3
-  resolution: "has-proto@npm:1.0.3"
-  checksum: 10c0/35a6989f81e9f8022c2f4027f8b48a552de714938765d019dbea6bb547bd49ce5010a3c7c32ec6ddac6e48fc546166a3583b128f5a7add8b058a6d8b4afec205
+"has-proto@npm:^1.2.0":
+  version: 1.2.0
+  resolution: "has-proto@npm:1.2.0"
+  dependencies:
+    dunder-proto: "npm:^1.0.0"
+  checksum: 10c0/46538dddab297ec2f43923c3d35237df45d8c55a6fc1067031e04c13ed8a9a8f94954460632fd4da84c31a1721eefee16d901cbb1ae9602bab93bb6e08f93b95
   languageName: node
   linkType: hard
 
-"has-symbols@npm:^1.0.1, has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3":
-  version: 1.0.3
-  resolution: "has-symbols@npm:1.0.3"
-  checksum: 10c0/e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3
+"has-symbols@npm:^1.0.1, has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "has-symbols@npm:1.1.0"
+  checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e
   languageName: node
   linkType: hard
 
@@ -9379,7 +9696,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2":
+"hasown@npm:^2.0.0, hasown@npm:^2.0.2":
   version: 2.0.2
   resolution: "hasown@npm:2.0.2"
   dependencies:
@@ -9420,7 +9737,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"hoist-non-react-statics@npm:3, hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
+"hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
   version: 3.3.2
   resolution: "hoist-non-react-statics@npm:3.3.2"
   dependencies:
@@ -9843,14 +10160,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"internal-slot@npm:^1.0.7":
-  version: 1.0.7
-  resolution: "internal-slot@npm:1.0.7"
+"internal-slot@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "internal-slot@npm:1.1.0"
   dependencies:
     es-errors: "npm:^1.3.0"
-    hasown: "npm:^2.0.0"
-    side-channel: "npm:^1.0.4"
-  checksum: 10c0/f8b294a4e6ea3855fc59551bbf35f2b832cf01fd5e6e2a97f5c201a071cc09b49048f856e484b67a6c721da5e55736c5b6ddafaf19e2dbeb4a3ff1821680de6c
+    hasown: "npm:^2.0.2"
+    side-channel: "npm:^1.1.0"
+  checksum: 10c0/03966f5e259b009a9bf1a78d60da920df198af4318ec004f57b8aef1dd3fe377fbc8cce63a96e8c810010302654de89f9e19de1cd8ad0061d15be28a695465c7
   languageName: node
   linkType: hard
 
@@ -9868,15 +10185,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"intl-messageformat@npm:10.7.14, intl-messageformat@npm:^10.3.5":
-  version: 10.7.14
-  resolution: "intl-messageformat@npm:10.7.14"
+"intl-messageformat@npm:10.7.16, intl-messageformat@npm:^10.7.16":
+  version: 10.7.16
+  resolution: "intl-messageformat@npm:10.7.16"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:2.3.2"
-    "@formatjs/fast-memoize": "npm:2.2.6"
-    "@formatjs/icu-messageformat-parser": "npm:2.11.0"
-    tslib: "npm:2"
-  checksum: 10c0/914c11118c47bb7d0163a7d54afebf52ae4fe9b250bba609b5db4c802a34f8bf758edbbc8c58094203205faf76948c7112399addb32e9b36e40483c4cf7c1cb7
+    "@formatjs/ecma402-abstract": "npm:2.3.4"
+    "@formatjs/fast-memoize": "npm:2.2.7"
+    "@formatjs/icu-messageformat-parser": "npm:2.11.2"
+    tslib: "npm:^2.8.0"
+  checksum: 10c0/537735bf6439f0560f132895d117df6839957ac04cdd58d861f6da86803d40bfc19059e3d341ddb8de87214b73a6329b57f9acdb512bb0f745dcf08729507b9b
   languageName: node
   linkType: hard
 
@@ -9890,8 +10207,8 @@ __metadata:
   linkType: hard
 
 "ioredis@npm:^5.3.2":
-  version: 5.4.2
-  resolution: "ioredis@npm:5.4.2"
+  version: 5.6.1
+  resolution: "ioredis@npm:5.6.1"
   dependencies:
     "@ioredis/commands": "npm:^1.1.1"
     cluster-key-slot: "npm:^1.1.0"
@@ -9902,7 +10219,7 @@ __metadata:
     redis-errors: "npm:^1.2.0"
     redis-parser: "npm:^3.0.0"
     standard-as-callback: "npm:^2.1.0"
-  checksum: 10c0/e59d2cceb43ed74b487d7b50fa91b93246e734e5d4835c7e62f64e44da072f12ab43b044248012e6f8b76c61a7c091a2388caad50e8ad69a8ce5515a730b23b8
+  checksum: 10c0/26ae49cf448e807e454a9bdea5a9dfdcf669e2fdbf2df341900a0fb693c5662fea7e39db3227ce8972d1bda0ba7da9b7410e5163b12d8878a579548d847220ac
   languageName: node
   linkType: hard
 
@@ -9969,13 +10286,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-array-buffer@npm:^3.0.4":
-  version: 3.0.4
-  resolution: "is-array-buffer@npm:3.0.4"
+"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5":
+  version: 3.0.5
+  resolution: "is-array-buffer@npm:3.0.5"
   dependencies:
-    call-bind: "npm:^1.0.2"
-    get-intrinsic: "npm:^1.2.1"
-  checksum: 10c0/42a49d006cc6130bc5424eae113e948c146f31f9d24460fc0958f855d9d810e6fd2e4519bf19aab75179af9c298ea6092459d8cafdec523cd19e529b26eab860
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
+    get-intrinsic: "npm:^1.2.6"
+  checksum: 10c0/c5c9f25606e86dbb12e756694afbbff64bc8b348d1bc989324c037e1068695131930199d6ad381952715dad3a9569333817f0b1a72ce5af7f883ce802e49c83d
   languageName: node
   linkType: hard
 
@@ -9995,12 +10313,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-bigint@npm:^1.0.1":
-  version: 1.0.4
-  resolution: "is-bigint@npm:1.0.4"
+"is-bigint@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "is-bigint@npm:1.1.0"
   dependencies:
-    has-bigints: "npm:^1.0.1"
-  checksum: 10c0/eb9c88e418a0d195ca545aff2b715c9903d9b0a5033bc5922fec600eb0c3d7b1ee7f882dbf2e0d5a6e694e42391be3683e4368737bd3c4a77f8ac293e7773696
+    has-bigints: "npm:^1.0.2"
+  checksum: 10c0/f4f4b905ceb195be90a6ea7f34323bf1c18e3793f18922e3e9a73c684c29eeeeff5175605c3a3a74cc38185fe27758f07efba3dbae812e5c5afbc0d2316b40e4
   languageName: node
   linkType: hard
 
@@ -10022,13 +10340,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-boolean-object@npm:^1.1.0":
-  version: 1.1.2
-  resolution: "is-boolean-object@npm:1.1.2"
+"is-boolean-object@npm:^1.2.1":
+  version: 1.2.2
+  resolution: "is-boolean-object@npm:1.2.2"
   dependencies:
-    call-bind: "npm:^1.0.2"
-    has-tostringtag: "npm:^1.0.0"
-  checksum: 10c0/6090587f8a8a8534c0f816da868bc94f32810f08807aa72fa7e79f7e11c466d281486ffe7a788178809c2aa71fe3e700b167fe80dd96dad68026bfff8ebf39f7
+    call-bound: "npm:^1.0.3"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/36ff6baf6bd18b3130186990026f5a95c709345c39cd368468e6c1b6ab52201e9fd26d8e1f4c066357b4938b0f0401e1a5000e08257787c1a02f3a719457001e
   languageName: node
   linkType: hard
 
@@ -10041,16 +10359,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-bun-module@npm:^1.0.2":
-  version: 1.1.0
-  resolution: "is-bun-module@npm:1.1.0"
+"is-bun-module@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "is-bun-module@npm:2.0.0"
   dependencies:
-    semver: "npm:^7.6.3"
-  checksum: 10c0/17cae968c3fe08e2bd66f8477e4d5a166d6299b5e7ce5c7558355551c50267f77dd386297fada6b68e4a32f01ce8920b0423e4d258242ea463b45901ec474beb
+    semver: "npm:^7.7.1"
+  checksum: 10c0/7d27a0679cfa5be1f5052650391f9b11040cd70c48d45112e312c56bc6b6ca9c9aea70dcce6cc40b1e8947bfff8567a5c5715d3b066fb478522dab46ea379240
   languageName: node
   linkType: hard
 
-"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7":
+"is-callable@npm:^1.1.3, is-callable@npm:^1.2.7":
   version: 1.2.7
   resolution: "is-callable@npm:1.2.7"
   checksum: 10c0/ceebaeb9d92e8adee604076971dd6000d38d6afc40bb843ea8e45c5579b57671c3f3b50d7f04869618242c6cee08d1b67806a8cb8edaaaf7c0748b3720d6066f
@@ -10084,21 +10402,24 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-data-view@npm:^1.0.1":
-  version: 1.0.1
-  resolution: "is-data-view@npm:1.0.1"
+"is-data-view@npm:^1.0.1, is-data-view@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "is-data-view@npm:1.0.2"
   dependencies:
+    call-bound: "npm:^1.0.2"
+    get-intrinsic: "npm:^1.2.6"
     is-typed-array: "npm:^1.1.13"
-  checksum: 10c0/a3e6ec84efe303da859107aed9b970e018e2bee7ffcb48e2f8096921a493608134240e672a2072577e5f23a729846241d9634806e8a0e51d9129c56d5f65442d
+  checksum: 10c0/ef3548a99d7e7f1370ce21006baca6d40c73e9f15c941f89f0049c79714c873d03b02dae1c64b3f861f55163ecc16da06506c5b8a1d4f16650b3d9351c380153
   languageName: node
   linkType: hard
 
-"is-date-object@npm:^1.0.1, is-date-object@npm:^1.0.5":
-  version: 1.0.5
-  resolution: "is-date-object@npm:1.0.5"
+"is-date-object@npm:^1.0.1, is-date-object@npm:^1.0.5, is-date-object@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "is-date-object@npm:1.1.0"
   dependencies:
-    has-tostringtag: "npm:^1.0.0"
-  checksum: 10c0/eed21e5dcc619c48ccef804dfc83a739dbb2abee6ca202838ee1bd5f760fe8d8a93444f0d49012ad19bb7c006186e2884a1b92f6e1c056da7fd23d0a9ad5992e
+    call-bound: "npm:^1.0.2"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/1a4d199c8e9e9cac5128d32e6626fa7805175af9df015620ac0d5d45854ccf348ba494679d872d37301032e35a54fc7978fba1687e8721b2139aea7870cafa2f
   languageName: node
   linkType: hard
 
@@ -10147,12 +10468,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-finalizationregistry@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "is-finalizationregistry@npm:1.0.2"
+"is-finalizationregistry@npm:^1.1.0":
+  version: 1.1.1
+  resolution: "is-finalizationregistry@npm:1.1.1"
   dependencies:
-    call-bind: "npm:^1.0.2"
-  checksum: 10c0/81caecc984d27b1a35c68741156fc651fb1fa5e3e6710d21410abc527eb226d400c0943a167922b2e920f6b3e58b0dede9aa795882b038b85f50b3a4b877db86
+    call-bound: "npm:^1.0.3"
+  checksum: 10c0/818dff679b64f19e228a8205a1e2d09989a98e98def3a817f889208cfcbf918d321b251aadf2c05918194803ebd2eb01b14fc9d0b2bea53d984f4137bfca5e97
   languageName: node
   linkType: hard
 
@@ -10227,7 +10548,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-map@npm:^2.0.1":
+"is-map@npm:^2.0.3":
   version: 2.0.3
   resolution: "is-map@npm:2.0.3"
   checksum: 10c0/2c4d431b74e00fdda7162cd8e4b763d6f6f217edf97d4f8538b94b8702b150610e2c64961340015fe8df5b1fcee33ccd2e9b62619c4a8a3a155f8de6d6d355fc
@@ -10241,19 +10562,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-negative-zero@npm:^2.0.3":
-  version: 2.0.3
-  resolution: "is-negative-zero@npm:2.0.3"
-  checksum: 10c0/bcdcf6b8b9714063ffcfa9929c575ac69bfdabb8f4574ff557dfc086df2836cf07e3906f5bbc4f2a5c12f8f3ba56af640c843cdfc74da8caed86c7c7d66fd08e
-  languageName: node
-  linkType: hard
-
-"is-number-object@npm:^1.0.4":
-  version: 1.0.7
-  resolution: "is-number-object@npm:1.0.7"
+"is-number-object@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "is-number-object@npm:1.1.1"
   dependencies:
-    has-tostringtag: "npm:^1.0.0"
-  checksum: 10c0/aad266da1e530f1804a2b7bd2e874b4869f71c98590b3964f9d06cc9869b18f8d1f4778f838ecd2a11011bce20aeecb53cb269ba916209b79c24580416b74b1b
+    call-bound: "npm:^1.0.3"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/97b451b41f25135ff021d85c436ff0100d84a039bb87ffd799cbcdbea81ef30c464ced38258cdd34f080be08fc3b076ca1f472086286d2aa43521d6ec6a79f53
   languageName: node
   linkType: hard
 
@@ -10305,13 +10620,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-path-inside@npm:^3.0.3":
-  version: 3.0.3
-  resolution: "is-path-inside@npm:3.0.3"
-  checksum: 10c0/cf7d4ac35fb96bab6a1d2c3598fe5ebb29aafb52c0aaa482b5a3ed9d8ba3edc11631e3ec2637660c44b3ce0e61a08d54946e8af30dec0b60a7c27296c68ffd05
-  languageName: node
-  linkType: hard
-
 "is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4":
   version: 2.0.4
   resolution: "is-plain-object@npm:2.0.4"
@@ -10335,13 +10643,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-regex@npm:^1.0.4, is-regex@npm:^1.1.4":
-  version: 1.1.4
-  resolution: "is-regex@npm:1.1.4"
+"is-regex@npm:^1.0.4, is-regex@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "is-regex@npm:1.2.1"
   dependencies:
-    call-bind: "npm:^1.0.2"
-    has-tostringtag: "npm:^1.0.0"
-  checksum: 10c0/bb72aae604a69eafd4a82a93002058c416ace8cde95873589a97fc5dac96a6c6c78a9977d487b7b95426a8f5073969124dd228f043f9f604f041f32fcc465fc1
+    call-bound: "npm:^1.0.2"
+    gopd: "npm:^1.2.0"
+    has-tostringtag: "npm:^1.0.2"
+    hasown: "npm:^2.0.2"
+  checksum: 10c0/1d3715d2b7889932349241680032e85d0b492cfcb045acb75ffc2c3085e8d561184f1f7e84b6f8321935b4aea39bc9c6ba74ed595b57ce4881a51dfdbc214e04
   languageName: node
   linkType: hard
 
@@ -10352,19 +10662,19 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-set@npm:^2.0.1":
+"is-set@npm:^2.0.3":
   version: 2.0.3
   resolution: "is-set@npm:2.0.3"
   checksum: 10c0/f73732e13f099b2dc879c2a12341cfc22ccaca8dd504e6edae26484bd5707a35d503fba5b4daad530a9b088ced1ae6c9d8200fd92e09b428fe14ea79ce8080b7
   languageName: node
   linkType: hard
 
-"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.3":
-  version: 1.0.3
-  resolution: "is-shared-array-buffer@npm:1.0.3"
+"is-shared-array-buffer@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "is-shared-array-buffer@npm:1.0.4"
   dependencies:
-    call-bind: "npm:^1.0.7"
-  checksum: 10c0/adc11ab0acbc934a7b9e5e9d6c588d4ec6682f6fea8cda5180721704fa32927582ede5b123349e32517fdadd07958973d24716c80e7ab198970c47acc09e59c7
+    call-bound: "npm:^1.0.3"
+  checksum: 10c0/65158c2feb41ff1edd6bbd6fd8403a69861cf273ff36077982b5d4d68e1d59278c71691216a4a64632bd76d4792d4d1d2553901b6666d84ade13bba5ea7bc7db
   languageName: node
   linkType: hard
 
@@ -10389,30 +10699,33 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-string@npm:^1.0.5, is-string@npm:^1.0.7":
-  version: 1.0.7
-  resolution: "is-string@npm:1.0.7"
+"is-string@npm:^1.0.7, is-string@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "is-string@npm:1.1.1"
   dependencies:
-    has-tostringtag: "npm:^1.0.0"
-  checksum: 10c0/905f805cbc6eedfa678aaa103ab7f626aac9ebbdc8737abb5243acaa61d9820f8edc5819106b8fcd1839e33db21de9f0116ae20de380c8382d16dc2a601921f6
+    call-bound: "npm:^1.0.3"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/2f518b4e47886bb81567faba6ffd0d8a8333cf84336e2e78bf160693972e32ad00fe84b0926491cc598dee576fdc55642c92e62d0cbe96bf36f643b6f956f94d
   languageName: node
   linkType: hard
 
-"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3":
-  version: 1.0.4
-  resolution: "is-symbol@npm:1.0.4"
+"is-symbol@npm:^1.0.4, is-symbol@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "is-symbol@npm:1.1.1"
   dependencies:
-    has-symbols: "npm:^1.0.2"
-  checksum: 10c0/9381dd015f7c8906154dbcbf93fad769de16b4b961edc94f88d26eb8c555935caa23af88bda0c93a18e65560f6d7cca0fd5a3f8a8e1df6f1abbb9bead4502ef7
+    call-bound: "npm:^1.0.2"
+    has-symbols: "npm:^1.1.0"
+    safe-regex-test: "npm:^1.1.0"
+  checksum: 10c0/f08f3e255c12442e833f75a9e2b84b2d4882fdfd920513cf2a4a2324f0a5b076c8fd913778e3ea5d258d5183e9d92c0cd20e04b03ab3df05316b049b2670af1e
   languageName: node
   linkType: hard
 
-"is-typed-array@npm:^1.1.13":
-  version: 1.1.13
-  resolution: "is-typed-array@npm:1.1.13"
+"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15":
+  version: 1.1.15
+  resolution: "is-typed-array@npm:1.1.15"
   dependencies:
-    which-typed-array: "npm:^1.1.14"
-  checksum: 10c0/fa5cb97d4a80e52c2cc8ed3778e39f175a1a2ae4ddf3adae3187d69586a1fd57cfa0b095db31f66aa90331e9e3da79184cea9c6abdcd1abc722dc3c3edd51cca
+    which-typed-array: "npm:^1.1.16"
+  checksum: 10c0/415511da3669e36e002820584e264997ffe277ff136643a3126cc949197e6ca3334d0f12d084e83b1994af2e9c8141275c741cf2b7da5a2ff62dd0cac26f76c4
   languageName: node
   linkType: hard
 
@@ -10423,29 +10736,29 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-weakmap@npm:^2.0.1":
-  version: 2.0.1
-  resolution: "is-weakmap@npm:2.0.1"
-  checksum: 10c0/9c9fec9efa7bf5030a4a927f33fff2a6976b93646259f92b517d3646c073cc5b98283a162ce75c412b060a46de07032444b530f0a4c9b6e012ef8f1741c3a987
-  languageName: node
-  linkType: hard
-
-"is-weakref@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "is-weakref@npm:1.0.2"
-  dependencies:
-    call-bind: "npm:^1.0.2"
-  checksum: 10c0/1545c5d172cb690c392f2136c23eec07d8d78a7f57d0e41f10078aa4f5daf5d7f57b6513a67514ab4f073275ad00c9822fc8935e00229d0a2089e1c02685d4b1
-  languageName: node
-  linkType: hard
-
-"is-weakset@npm:^2.0.1":
+"is-weakmap@npm:^2.0.2":
   version: 2.0.2
-  resolution: "is-weakset@npm:2.0.2"
+  resolution: "is-weakmap@npm:2.0.2"
+  checksum: 10c0/443c35bb86d5e6cc5929cd9c75a4024bb0fff9586ed50b092f94e700b89c43a33b186b76dbc6d54f3d3d09ece689ab38dcdc1af6a482cbe79c0f2da0a17f1299
+  languageName: node
+  linkType: hard
+
+"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0":
+  version: 1.1.1
+  resolution: "is-weakref@npm:1.1.1"
   dependencies:
-    call-bind: "npm:^1.0.2"
-    get-intrinsic: "npm:^1.1.1"
-  checksum: 10c0/ef5136bd446ae4603229b897f73efd0720c6ab3ec6cc05c8d5c4b51aa9f95164713c4cad0a22ff1fedf04865ff86cae4648bc1d5eead4b6388e1150525af1cc1
+    call-bound: "npm:^1.0.3"
+  checksum: 10c0/8e0a9c07b0c780949a100e2cab2b5560a48ecd4c61726923c1a9b77b6ab0aa0046c9e7fb2206042296817045376dee2c8ab1dabe08c7c3dfbf195b01275a085b
+  languageName: node
+  linkType: hard
+
+"is-weakset@npm:^2.0.3":
+  version: 2.0.4
+  resolution: "is-weakset@npm:2.0.4"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    get-intrinsic: "npm:^1.2.6"
+  checksum: 10c0/6491eba08acb8dc9532da23cb226b7d0192ede0b88f16199e592e4769db0a077119c1f5d2283d1e0d16d739115f70046e887e477eb0e66cd90e1bb29f28ba647
   languageName: node
   linkType: hard
 
@@ -10579,16 +10892,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"iterator.prototype@npm:^1.1.3":
-  version: 1.1.3
-  resolution: "iterator.prototype@npm:1.1.3"
+"iterator.prototype@npm:^1.1.4":
+  version: 1.1.5
+  resolution: "iterator.prototype@npm:1.1.5"
   dependencies:
-    define-properties: "npm:^1.2.1"
-    get-intrinsic: "npm:^1.2.1"
-    has-symbols: "npm:^1.0.3"
-    reflect.getprototypeof: "npm:^1.0.4"
-    set-function-name: "npm:^2.0.1"
-  checksum: 10c0/68b0320c14291fbb3d8ed5a17e255d3127e7971bec19108076667e79c9ff4c7d69f99de4b0b3075c789c3f318366d7a0a35bb086eae0f2cf832dd58465b2f9e6
+    define-data-property: "npm:^1.1.4"
+    es-object-atoms: "npm:^1.0.0"
+    get-intrinsic: "npm:^1.2.6"
+    get-proto: "npm:^1.0.0"
+    has-symbols: "npm:^1.1.0"
+    set-function-name: "npm:^2.0.2"
+  checksum: 10c0/f7a262808e1b41049ab55f1e9c29af7ec1025a000d243b83edf34ce2416eedd56079b117fa59376bb4a724110690f13aa8427f2ee29a09eec63a7e72367626d0
   languageName: node
   linkType: hard
 
@@ -11174,13 +11488,12 @@ __metadata:
   linkType: hard
 
 "jsdom@npm:^26.0.0":
-  version: 26.0.0
-  resolution: "jsdom@npm:26.0.0"
+  version: 26.1.0
+  resolution: "jsdom@npm:26.1.0"
   dependencies:
     cssstyle: "npm:^4.2.1"
     data-urls: "npm:^5.0.0"
-    decimal.js: "npm:^10.4.3"
-    form-data: "npm:^4.0.1"
+    decimal.js: "npm:^10.5.0"
     html-encoding-sniffer: "npm:^4.0.0"
     http-proxy-agent: "npm:^7.0.2"
     https-proxy-agent: "npm:^7.0.6"
@@ -11190,12 +11503,12 @@ __metadata:
     rrweb-cssom: "npm:^0.8.0"
     saxes: "npm:^6.0.0"
     symbol-tree: "npm:^3.2.4"
-    tough-cookie: "npm:^5.0.0"
+    tough-cookie: "npm:^5.1.1"
     w3c-xmlserializer: "npm:^5.0.0"
     webidl-conversions: "npm:^7.0.0"
     whatwg-encoding: "npm:^3.1.1"
     whatwg-mimetype: "npm:^4.0.0"
-    whatwg-url: "npm:^14.1.0"
+    whatwg-url: "npm:^14.1.1"
     ws: "npm:^8.18.0"
     xml-name-validator: "npm:^5.0.0"
   peerDependencies:
@@ -11203,7 +11516,7 @@ __metadata:
   peerDependenciesMeta:
     canvas:
       optional: true
-  checksum: 10c0/e48725ba4027edcfc9bca5799eaec72c6561ecffe3675a8ff87fe9c3541ca4ff9f82b4eff5b3d9c527302da0d859b2f60e9364347a5d42b77f5c76c436c569dc
+  checksum: 10c0/5b14a5bc32ce077a06fb42d1ab95b1191afa5cbbce8859e3b96831c5143becbbcbf0511d4d4934e922d2901443ced2cdc3b734c1cf30b5f73b3e067ce457d0f4
   languageName: node
   linkType: hard
 
@@ -11265,15 +11578,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"json-stable-stringify@npm:1, json-stable-stringify@npm:^1.0.1":
-  version: 1.1.1
-  resolution: "json-stable-stringify@npm:1.1.1"
+"json-stable-stringify@npm:^1.1.1":
+  version: 1.2.1
+  resolution: "json-stable-stringify@npm:1.2.1"
   dependencies:
-    call-bind: "npm:^1.0.5"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
     isarray: "npm:^2.0.5"
     jsonify: "npm:^0.0.1"
     object-keys: "npm:^1.1.1"
-  checksum: 10c0/3801e3eeccbd030afb970f54bea690a079cfea7d9ed206a1b17ca9367f4b7772c764bf77a48f03e56b50e5f7ee7d11c52339fe20d8d7ccead003e4ca69e4cfde
+  checksum: 10c0/e623e7ce89282f089d56454087edb717357e8572089b552fbc6980fb7814dc3943f7d0e4f1a19429a36ce9f4428b6c8ee6883357974457aaaa98daba5adebeea
   languageName: node
   linkType: hard
 
@@ -11343,7 +11657,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"keyv@npm:^4.5.3, keyv@npm:^4.5.4":
+"keyv@npm:^4.5.4":
   version: 4.5.4
   resolution: "keyv@npm:4.5.4"
   dependencies:
@@ -11801,9 +12115,16 @@ __metadata:
   linkType: hard
 
 "marky@npm:^1.2.5":
-  version: 1.2.5
-  resolution: "marky@npm:1.2.5"
-  checksum: 10c0/ca8a011f287dab1ac3291df720fc32b366c4cd767347b63722966650405ce71ec6566f71d1e22e1768bf6461a7fd689b9038e7df0fcfb62eacf3a5a6dcac249e
+  version: 1.3.0
+  resolution: "marky@npm:1.3.0"
+  checksum: 10c0/6619cdb132fdc4f7cd3e2bed6eebf81a38e50ff4b426bbfb354db68731e4adfebf35ebfd7c8e5a6e846cbf9b872588c4f76db25782caee8c1529ec9d483bf98b
+  languageName: node
+  linkType: hard
+
+"math-intrinsics@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "math-intrinsics@npm:1.1.0"
+  checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f
   languageName: node
   linkType: hard
 
@@ -12068,7 +12389,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
+"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
   version: 3.1.2
   resolution: "minimatch@npm:3.1.2"
   dependencies:
@@ -12427,10 +12748,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"node-releases@npm:^2.0.18":
-  version: 2.0.18
-  resolution: "node-releases@npm:2.0.18"
-  checksum: 10c0/786ac9db9d7226339e1dc84bbb42007cb054a346bd9257e6aa154d294f01bc6a6cddb1348fa099f079be6580acbb470e3c048effd5f719325abd0179e566fd27
+"node-releases@npm:^2.0.19":
+  version: 2.0.19
+  resolution: "node-releases@npm:2.0.19"
+  checksum: 10c0/52a0dbd25ccf545892670d1551690fe0facb6a471e15f2cfa1b20142a5b255b3aa254af5f59d6ecb69c2bec7390bc643c43aa63b13bf5e64b6075952e716b1aa
   languageName: node
   linkType: hard
 
@@ -12538,10 +12859,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"object-inspect@npm:^1.13.1":
-  version: 1.13.1
-  resolution: "object-inspect@npm:1.13.1"
-  checksum: 10c0/fad603f408e345c82e946abdf4bfd774260a5ed3e5997a0b057c44153ac32c7271ff19e3a5ae39c858da683ba045ccac2f65245c12763ce4e8594f818f4a648d
+"object-inspect@npm:^1.13.3":
+  version: 1.13.4
+  resolution: "object-inspect@npm:1.13.4"
+  checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692
   languageName: node
   linkType: hard
 
@@ -12571,15 +12892,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"object.assign@npm:^4.1.4, object.assign@npm:^4.1.5":
-  version: 4.1.5
-  resolution: "object.assign@npm:4.1.5"
+"object.assign@npm:^4.1.4, object.assign@npm:^4.1.7":
+  version: 4.1.7
+  resolution: "object.assign@npm:4.1.7"
   dependencies:
-    call-bind: "npm:^1.0.5"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
     define-properties: "npm:^1.2.1"
-    has-symbols: "npm:^1.0.3"
+    es-object-atoms: "npm:^1.0.0"
+    has-symbols: "npm:^1.1.0"
     object-keys: "npm:^1.1.1"
-  checksum: 10c0/60108e1fa2706f22554a4648299b0955236c62b3685c52abf4988d14fffb0e7731e00aa8c6448397e3eb63d087dcc124a9f21e1980f36d0b2667f3c18bacd469
+  checksum: 10c0/3b2732bd860567ea2579d1567525168de925a8d852638612846bd8082b3a1602b7b89b67b09913cbb5b9bd6e95923b2ae73580baa9d99cb4e990564e8cbf5ddc
   languageName: node
   linkType: hard
 
@@ -12639,14 +12962,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"object.values@npm:^1.1.0, object.values@npm:^1.1.6, object.values@npm:^1.2.0":
-  version: 1.2.0
-  resolution: "object.values@npm:1.2.0"
+"object.values@npm:^1.1.0, object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "object.values@npm:1.2.1"
   dependencies:
-    call-bind: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
     define-properties: "npm:^1.2.1"
     es-object-atoms: "npm:^1.0.0"
-  checksum: 10c0/15809dc40fd6c5529501324fec5ff08570b7d70fb5ebbe8e2b3901afec35cf2b3dc484d1210c6c642cd3e7e0a5e18dd1d6850115337fef46bdae14ab0cb18ac3
+  checksum: 10c0/3c47814fdc64842ae3d5a74bc9d06bdd8d21563c04d9939bf6716a9c00596a4ebc342552f8934013d1ec991c74e3671b26710a0c51815f0b603795605ab6b2c9
   languageName: node
   linkType: hard
 
@@ -12764,6 +13088,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"own-keys@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "own-keys@npm:1.0.1"
+  dependencies:
+    get-intrinsic: "npm:^1.2.6"
+    object-keys: "npm:^1.1.1"
+    safe-push-apply: "npm:^1.0.0"
+  checksum: 10c0/6dfeb3455bff92ec3f16a982d4e3e65676345f6902d9f5ded1d8265a6318d0200ce461956d6d1c70053c7fe9f9fe65e552faac03f8140d37ef0fdd108e67013a
+  languageName: node
+  linkType: hard
+
 "p-finally@npm:^1.0.0":
   version: 1.0.0
   resolution: "p-finally@npm:1.0.0"
@@ -13059,20 +13394,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"performance-now@npm:^0.2.0":
-  version: 0.2.0
-  resolution: "performance-now@npm:0.2.0"
-  checksum: 10c0/d7f3824e443491208f7124b45d3280dbff889f8f048c3aee507109c24644d51a226eb07fd7ac51dd0eef144639590c57410c2d167bd4fdf0c5caa0101a449c3d
-  languageName: node
-  linkType: hard
-
-"performance-now@npm:^2.1.0":
-  version: 2.1.0
-  resolution: "performance-now@npm:2.1.0"
-  checksum: 10c0/22c54de06f269e29f640e0e075207af57de5052a3d15e360c09b9a8663f393f6f45902006c1e71aa8a5a1cdfb1a47fe268826f8496d6425c362f00f5bc3e85d9
-  languageName: node
-  linkType: hard
-
 "pg-cloudflare@npm:^1.1.1":
   version: 1.1.1
   resolution: "pg-cloudflare@npm:1.1.1"
@@ -13101,19 +13422,26 @@ __metadata:
   languageName: node
   linkType: hard
 
-"pg-pool@npm:^3.7.0":
-  version: 3.7.0
-  resolution: "pg-pool@npm:3.7.0"
+"pg-pool@npm:^3.7.1":
+  version: 3.7.1
+  resolution: "pg-pool@npm:3.7.1"
   peerDependencies:
     pg: ">=8.0"
-  checksum: 10c0/9128673cf941f288c0cb1a74ca959a9b4f6075ef73b2cc7dece5d4db3dd7043784869e7c12bce2e69ca0df22132a419cc45c2050b4373632856fe8bae9eb94b5
+  checksum: 10c0/65bff013102684774f4cfdffbfe0a2b0614252234561d391608f7fd915477e44f5fd0e1968ecc2f42032dcf8bac241d2f724c4f3df90384567d22df37dd3b6ba
   languageName: node
   linkType: hard
 
-"pg-protocol@npm:*, pg-protocol@npm:^1.7.0":
-  version: 1.7.0
-  resolution: "pg-protocol@npm:1.7.0"
-  checksum: 10c0/c4af854d9b843c808231c0040fed89f2b9101006157df8da2bb2f62a7dde702de748d852228dc22df41cc7ffddfb526af3bcb34b278b581e9f76a060789186c1
+"pg-protocol@npm:*":
+  version: 1.8.0
+  resolution: "pg-protocol@npm:1.8.0"
+  checksum: 10c0/2be784955599d84b564795952cee52cc2b8eab0be43f74fc1061506353801e282c1d52c9e0691a9b72092c1f3fde370e9b181e80fef6bb82a9b8d1618bfa91e6
+  languageName: node
+  linkType: hard
+
+"pg-protocol@npm:^1.7.1":
+  version: 1.7.1
+  resolution: "pg-protocol@npm:1.7.1"
+  checksum: 10c0/3168d407ddc4c0fa2403eb9b49205399d4bc53dadbafdfcc5d25fa61b860a31c25df25704cf14c8140c80f0a41061d586e5fd5ce9bf800dfb91e9ce810bc2c37
   languageName: node
   linkType: hard
 
@@ -13146,13 +13474,13 @@ __metadata:
   linkType: hard
 
 "pg@npm:^8.5.0":
-  version: 8.13.1
-  resolution: "pg@npm:8.13.1"
+  version: 8.13.3
+  resolution: "pg@npm:8.13.3"
   dependencies:
     pg-cloudflare: "npm:^1.1.1"
     pg-connection-string: "npm:^2.7.0"
-    pg-pool: "npm:^3.7.0"
-    pg-protocol: "npm:^1.7.0"
+    pg-pool: "npm:^3.7.1"
+    pg-protocol: "npm:^1.7.1"
     pg-types: "npm:^2.1.0"
     pgpass: "npm:1.x"
   peerDependencies:
@@ -13163,7 +13491,7 @@ __metadata:
   peerDependenciesMeta:
     pg-native:
       optional: true
-  checksum: 10c0/c13bc661cbdb115337bc8519254836faf4bd79106dfd7ed588c8ece8c8b2dd3b7376bfec9a9a2f7646fa095b0b310cec77a83c3ba2ea4872331446eb93fd9055
+  checksum: 10c0/7296f0e5930b35faef471be2673210cda553b30f1b8e9d176fcc286aa43248e17e09336032bf5a6bba55d2cc2d03afb8a407b5a6e6bc56ebb331c02d1a7ccc05
   languageName: node
   linkType: hard
 
@@ -13176,13 +13504,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.1":
+"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1":
   version: 1.1.1
   resolution: "picocolors@npm:1.1.1"
   checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
   languageName: node
   linkType: hard
 
+"picomatch@npm:2 || 3 || 4, picomatch@npm:^4.0.2":
+  version: 4.0.2
+  resolution: "picomatch@npm:4.0.2"
+  checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc
+  languageName: node
+  linkType: hard
+
 "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.2, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1":
   version: 2.3.1
   resolution: "picomatch@npm:2.3.1"
@@ -13385,18 +13720,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-color-functional-notation@npm:^7.0.7":
-  version: 7.0.7
-  resolution: "postcss-color-functional-notation@npm:7.0.7"
+"postcss-color-functional-notation@npm:^7.0.8":
+  version: 7.0.8
+  resolution: "postcss-color-functional-notation@npm:7.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
     "@csstools/utilities": "npm:^2.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/d9f64abb54da1e2e2641d68548e5fe3685e1b5f85cd39059f1e9312f9c897eef80a76d1e9b9271d4700a5954fc0c0b6494fc8ed4a25fe64a25525b3973be4a36
+  checksum: 10c0/4180e2f6ee9c925d6c47e727cfc50de2186d4a5cfda6e1ccf28f60e5536b418ddd90f9cc5f9cbcd1900f74098101bca8f844867e16b591e66760300e34257e47
   languageName: node
   linkType: hard
 
@@ -13607,18 +13942,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-lab-function@npm:^7.0.7":
-  version: 7.0.7
-  resolution: "postcss-lab-function@npm:7.0.7"
+"postcss-lab-function@npm:^7.0.8":
+  version: 7.0.8
+  resolution: "postcss-lab-function@npm:7.0.8"
   dependencies:
-    "@csstools/css-color-parser": "npm:^3.0.7"
+    "@csstools/css-color-parser": "npm:^3.0.8"
     "@csstools/css-parser-algorithms": "npm:^3.0.4"
     "@csstools/css-tokenizer": "npm:^3.0.3"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
     "@csstools/utilities": "npm:^2.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/bee0f50c3f59da04b219b528cdd63b8f2600fd9bbaa02ba101593f29f4cd090c25acc40254a41a11e3db221cc98b7a1b2600fd3abe3065262c2cb5c7501a3dba
+  checksum: 10c0/5f7b6f95cb3d1aa099c16dcdd89c575f112387600f30949f74c205e0846c9303ca851be794fad9fd56825859d38ac811f972cc34bbc2dfcf71371c640165ddfb
   languageName: node
   linkType: hard
 
@@ -13638,14 +13973,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-logical@npm:^8.0.0":
-  version: 8.0.0
-  resolution: "postcss-logical@npm:8.0.0"
+"postcss-logical@npm:^8.1.0":
+  version: 8.1.0
+  resolution: "postcss-logical@npm:8.1.0"
   dependencies:
     postcss-value-parser: "npm:^4.2.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/2caa04e45227ab9dec728416ccde47514e1c347ee72aac58e13ecee3bc7fbc8b53e3fe4f1e2e4396432feb1d54e70a1f06ec5a74d60e84bafff05ab82f196475
+  checksum: 10c0/0e2e9e901d8a550db7f682d46b1f7e4f363c1ada061dc8e4548e2b563c5e39f3684a2d7c3f11fe061188782bca37874e34967fc6179fa6d98a49ff66a0076d27
   languageName: node
   linkType: hard
 
@@ -13939,20 +14274,20 @@ __metadata:
   linkType: hard
 
 "postcss-preset-env@npm:^10.0.0":
-  version: 10.1.3
-  resolution: "postcss-preset-env@npm:10.1.3"
+  version: 10.1.5
+  resolution: "postcss-preset-env@npm:10.1.5"
   dependencies:
     "@csstools/postcss-cascade-layers": "npm:^5.0.1"
-    "@csstools/postcss-color-function": "npm:^4.0.7"
-    "@csstools/postcss-color-mix-function": "npm:^3.0.7"
+    "@csstools/postcss-color-function": "npm:^4.0.8"
+    "@csstools/postcss-color-mix-function": "npm:^3.0.8"
     "@csstools/postcss-content-alt-text": "npm:^2.0.4"
-    "@csstools/postcss-exponential-functions": "npm:^2.0.6"
+    "@csstools/postcss-exponential-functions": "npm:^2.0.7"
     "@csstools/postcss-font-format-keywords": "npm:^4.0.0"
-    "@csstools/postcss-gamut-mapping": "npm:^2.0.7"
-    "@csstools/postcss-gradients-interpolation-method": "npm:^5.0.7"
-    "@csstools/postcss-hwb-function": "npm:^4.0.7"
+    "@csstools/postcss-gamut-mapping": "npm:^2.0.8"
+    "@csstools/postcss-gradients-interpolation-method": "npm:^5.0.8"
+    "@csstools/postcss-hwb-function": "npm:^4.0.8"
     "@csstools/postcss-ic-unit": "npm:^4.0.0"
-    "@csstools/postcss-initial": "npm:^2.0.0"
+    "@csstools/postcss-initial": "npm:^2.0.1"
     "@csstools/postcss-is-pseudo-class": "npm:^5.0.1"
     "@csstools/postcss-light-dark-function": "npm:^2.0.7"
     "@csstools/postcss-logical-float-and-clear": "npm:^3.0.0"
@@ -13960,29 +14295,29 @@ __metadata:
     "@csstools/postcss-logical-overscroll-behavior": "npm:^2.0.0"
     "@csstools/postcss-logical-resize": "npm:^3.0.0"
     "@csstools/postcss-logical-viewport-units": "npm:^3.0.3"
-    "@csstools/postcss-media-minmax": "npm:^2.0.6"
+    "@csstools/postcss-media-minmax": "npm:^2.0.7"
     "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^3.0.4"
     "@csstools/postcss-nested-calc": "npm:^4.0.0"
     "@csstools/postcss-normalize-display-values": "npm:^4.0.0"
-    "@csstools/postcss-oklab-function": "npm:^4.0.7"
+    "@csstools/postcss-oklab-function": "npm:^4.0.8"
     "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0"
-    "@csstools/postcss-random-function": "npm:^1.0.2"
-    "@csstools/postcss-relative-color-syntax": "npm:^3.0.7"
+    "@csstools/postcss-random-function": "npm:^1.0.3"
+    "@csstools/postcss-relative-color-syntax": "npm:^3.0.8"
     "@csstools/postcss-scope-pseudo-class": "npm:^4.0.1"
-    "@csstools/postcss-sign-functions": "npm:^1.1.1"
-    "@csstools/postcss-stepped-value-functions": "npm:^4.0.6"
-    "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.1"
-    "@csstools/postcss-trigonometric-functions": "npm:^4.0.6"
+    "@csstools/postcss-sign-functions": "npm:^1.1.2"
+    "@csstools/postcss-stepped-value-functions": "npm:^4.0.7"
+    "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.2"
+    "@csstools/postcss-trigonometric-functions": "npm:^4.0.7"
     "@csstools/postcss-unset-value": "npm:^4.0.0"
     autoprefixer: "npm:^10.4.19"
-    browserslist: "npm:^4.23.1"
+    browserslist: "npm:^4.24.4"
     css-blank-pseudo: "npm:^7.0.1"
     css-has-pseudo: "npm:^7.0.2"
     css-prefers-color-scheme: "npm:^10.0.0"
     cssdb: "npm:^8.2.3"
     postcss-attribute-case-insensitive: "npm:^7.0.1"
     postcss-clamp: "npm:^4.1.0"
-    postcss-color-functional-notation: "npm:^7.0.7"
+    postcss-color-functional-notation: "npm:^7.0.8"
     postcss-color-hex-alpha: "npm:^10.0.0"
     postcss-color-rebeccapurple: "npm:^10.0.0"
     postcss-custom-media: "npm:^11.0.5"
@@ -13995,8 +14330,8 @@ __metadata:
     postcss-font-variant: "npm:^5.0.0"
     postcss-gap-properties: "npm:^6.0.0"
     postcss-image-set-function: "npm:^7.0.0"
-    postcss-lab-function: "npm:^7.0.7"
-    postcss-logical: "npm:^8.0.0"
+    postcss-lab-function: "npm:^7.0.8"
+    postcss-logical: "npm:^8.1.0"
     postcss-nesting: "npm:^13.0.1"
     postcss-opacity-percentage: "npm:^3.0.0"
     postcss-overflow-shorthand: "npm:^6.0.0"
@@ -14007,7 +14342,7 @@ __metadata:
     postcss-selector-not: "npm:^8.0.1"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/0ae02015ad3ac69e8ef26afc1a06cb9fbb400104eca5c69a4baa20925e06364712f05b5d87ec9cf9445256e62344e6c2bad8d261a09b35a0e982e055564e3ba8
+  checksum: 10c0/5ed5aeb7c9718230742a56d9b49e05a90135bc4bb77f97d9978bdb0b999d36a2d6175d99360c966cb7a307c9efe4b8792f4c0b79ec99a233f9e1c1ebae4244f0
   languageName: node
   linkType: hard
 
@@ -14141,13 +14476,13 @@ __metadata:
   linkType: hard
 
 "postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.49":
-  version: 8.5.1
-  resolution: "postcss@npm:8.5.1"
+  version: 8.5.3
+  resolution: "postcss@npm:8.5.3"
   dependencies:
     nanoid: "npm:^3.3.8"
     picocolors: "npm:^1.1.1"
     source-map-js: "npm:^1.2.1"
-  checksum: 10c0/c4d90c59c98e8a0c102b77d3f4cac190f883b42d63dc60e2f3ed840f16197c0c8e25a4327d2e9a847b45a985612317dc0534178feeebd0a1cf3eb0eecf75cae4
+  checksum: 10c0/b75510d7b28c3ab728c8733dd01538314a18c52af426f199a3c9177e63eb08602a3938bfb66b62dc01350b9aed62087eabbf229af97a1659eb8d3513cec823b3
   languageName: node
   linkType: hard
 
@@ -14328,7 +14663,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
+"prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
   version: 15.8.1
   resolution: "prop-types@npm:15.8.1"
   dependencies:
@@ -14466,15 +14801,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"raf@npm:^3.1.0":
-  version: 3.4.1
-  resolution: "raf@npm:3.4.1"
-  dependencies:
-    performance-now: "npm:^2.1.0"
-  checksum: 10c0/337f0853c9e6a77647b0f499beedafea5d6facfb9f2d488a624f88b03df2be72b8a0e7f9118a3ff811377d534912039a3311815700d2b6d2313f82f736f9eb6e
-  languageName: node
-  linkType: hard
-
 "randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0":
   version: 2.1.0
   resolution: "randombytes@npm:2.1.0"
@@ -14597,25 +14923,25 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-intl@npm:^7.0.0":
-  version: 7.1.5
-  resolution: "react-intl@npm:7.1.5"
+"react-intl@npm:^7.1.10":
+  version: 7.1.10
+  resolution: "react-intl@npm:7.1.10"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:2.3.2"
-    "@formatjs/icu-messageformat-parser": "npm:2.11.0"
-    "@formatjs/intl": "npm:3.1.3"
-    "@types/hoist-non-react-statics": "npm:3"
+    "@formatjs/ecma402-abstract": "npm:2.3.4"
+    "@formatjs/icu-messageformat-parser": "npm:2.11.2"
+    "@formatjs/intl": "npm:3.1.6"
+    "@types/hoist-non-react-statics": "npm:^3.3.1"
     "@types/react": "npm:16 || 17 || 18 || 19"
-    hoist-non-react-statics: "npm:3"
-    intl-messageformat: "npm:10.7.14"
-    tslib: "npm:2"
+    hoist-non-react-statics: "npm:^3.3.2"
+    intl-messageformat: "npm:10.7.16"
+    tslib: "npm:^2.8.0"
   peerDependencies:
-    react: ^16.6.0 || 17 || 18 || 19
-    typescript: 5
+    react: 16 || 17 || 18 || 19
+    typescript: ^5.6.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/9d8d5afae16d26466f82fc1c0b8c4afd7559a8faa5b3ef31983a0af540b1b225fa89208b48bf9ec83056c0a27f2972fb86f2717422f788efdc819fb0e0846134
+  checksum: 10c0/7d47ac7e2284eb6d920d26d5cdcfee33f54805eb27284dab969ac931139c5eacb2b24f60c8e7645a623b768d0fdfa5dfc4df63a9270301d57aa356ddf231c484
   languageName: node
   linkType: hard
 
@@ -14647,30 +14973,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-motion@npm:^0.5.2":
-  version: 0.5.2
-  resolution: "react-motion@npm:0.5.2"
-  dependencies:
-    performance-now: "npm:^0.2.0"
-    prop-types: "npm:^15.5.8"
-    raf: "npm:^3.1.0"
-  peerDependencies:
-    react: ^0.14.9 || ^15.3.0 || ^16.0.0
-  checksum: 10c0/4ea6f1cc7079f0161fd786cc755133a822d87d9c0510369b8fb348d9ad602111efa2e3496dbcc390c967229e39e3eb5f6dd5dd6d3d124289443de31d6035a6c8
-  languageName: node
-  linkType: hard
-
-"react-notification@npm:^6.8.5":
-  version: 6.8.5
-  resolution: "react-notification@npm:6.8.5"
-  dependencies:
-    prop-types: "npm:^15.6.2"
-  peerDependencies:
-    react: ^0.14.0 || ^15.0.0 || ^16.0.0
-  checksum: 10c0/14ffb71a5b18301830699b814d1de2421f4f43f31df5b95efd95cd47548a0d7597ec58abc16a12191958cad398495eba9274193af3294863e2864d32ea79f2c6
-  languageName: node
-  linkType: hard
-
 "react-overlays@npm:^5.2.1":
   version: 5.2.1
   resolution: "react-overlays@npm:5.2.1"
@@ -14776,8 +15078,8 @@ __metadata:
   linkType: hard
 
 "react-select@npm:^5.7.3":
-  version: 5.10.0
-  resolution: "react-select@npm:5.10.0"
+  version: 5.10.1
+  resolution: "react-select@npm:5.10.1"
   dependencies:
     "@babel/runtime": "npm:^7.12.0"
     "@emotion/cache": "npm:^11.4.0"
@@ -14791,7 +15093,7 @@ __metadata:
   peerDependencies:
     react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
     react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-  checksum: 10c0/64cc73ef43556d0a199420d7d19f9f72e3c5e3a7f6828aef5421ec16cc0e4bc337061a8fa3c03afc5b929a087a4ca866f497e0ef865b03fe014c5cacde5e71dd
+  checksum: 10c0/0d10a249b96150bd648f2575d59c848b8fac7f4d368a97ae84e4aaba5bbc1035deba4cdc82e49a43904b79ec50494505809618b0e98022b2d51e7629551912ed
   languageName: node
   linkType: hard
 
@@ -15050,17 +15352,19 @@ __metadata:
   languageName: node
   linkType: hard
 
-"reflect.getprototypeof@npm:^1.0.4":
-  version: 1.0.4
-  resolution: "reflect.getprototypeof@npm:1.0.4"
+"reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9":
+  version: 1.0.10
+  resolution: "reflect.getprototypeof@npm:1.0.10"
   dependencies:
-    call-bind: "npm:^1.0.2"
-    define-properties: "npm:^1.2.0"
-    es-abstract: "npm:^1.22.1"
-    get-intrinsic: "npm:^1.2.1"
-    globalthis: "npm:^1.0.3"
-    which-builtin-type: "npm:^1.1.3"
-  checksum: 10c0/02104cdd22658b637efe6b1df73658edab539268347327c8250a72d0cb273dcdf280c284e2d94155d22601d022d16be1a816a8616d679e447cbcbde9860d15cb
+    call-bind: "npm:^1.0.8"
+    define-properties: "npm:^1.2.1"
+    es-abstract: "npm:^1.23.9"
+    es-errors: "npm:^1.3.0"
+    es-object-atoms: "npm:^1.0.0"
+    get-intrinsic: "npm:^1.2.7"
+    get-proto: "npm:^1.0.1"
+    which-builtin-type: "npm:^1.2.1"
+  checksum: 10c0/7facec28c8008876f8ab98e80b7b9cb4b1e9224353fd4756dda5f2a4ab0d30fa0a5074777c6df24e1e0af463a2697513b0a11e548d99cf52f21f7bc6ba48d3ac
   languageName: node
   linkType: hard
 
@@ -15120,15 +15424,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.5.2":
-  version: 1.5.2
-  resolution: "regexp.prototype.flags@npm:1.5.2"
+"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.5.3":
+  version: 1.5.4
+  resolution: "regexp.prototype.flags@npm:1.5.4"
   dependencies:
-    call-bind: "npm:^1.0.6"
+    call-bind: "npm:^1.0.8"
     define-properties: "npm:^1.2.1"
     es-errors: "npm:^1.3.0"
-    set-function-name: "npm:^2.0.1"
-  checksum: 10c0/0f3fc4f580d9c349f8b560b012725eb9c002f36daa0041b3fbf6f4238cb05932191a4d7d5db3b5e2caa336d5150ad0402ed2be81f711f9308fe7e1a9bf9bd552
+    get-proto: "npm:^1.0.1"
+    gopd: "npm:^1.2.0"
+    set-function-name: "npm:^2.0.2"
+  checksum: 10c0/83b88e6115b4af1c537f8dabf5c3744032cb875d63bc05c288b1b8c0ef37cbe55353f95d8ca817e8843806e3e150b118bc624e4279b24b4776b4198232735a77
   languageName: node
   linkType: hard
 
@@ -15463,15 +15769,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"safe-array-concat@npm:^1.0.0, safe-array-concat@npm:^1.1.2":
-  version: 1.1.2
-  resolution: "safe-array-concat@npm:1.1.2"
+"safe-array-concat@npm:^1.0.0, safe-array-concat@npm:^1.1.3":
+  version: 1.1.3
+  resolution: "safe-array-concat@npm:1.1.3"
   dependencies:
-    call-bind: "npm:^1.0.7"
-    get-intrinsic: "npm:^1.2.4"
-    has-symbols: "npm:^1.0.3"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.2"
+    get-intrinsic: "npm:^1.2.6"
+    has-symbols: "npm:^1.1.0"
     isarray: "npm:^2.0.5"
-  checksum: 10c0/12f9fdb01c8585e199a347eacc3bae7b5164ae805cdc8c6707199dbad5b9e30001a50a43c4ee24dc9ea32dbb7279397850e9208a7e217f4d8b1cf5d90129dec9
+  checksum: 10c0/43c86ffdddc461fb17ff8a17c5324f392f4868f3c7dd2c6a5d9f5971713bc5fd755667212c80eab9567595f9a7509cc2f83e590ddaebd1bd19b780f9c79f9a8d
   languageName: node
   linkType: hard
 
@@ -15489,14 +15796,24 @@ __metadata:
   languageName: node
   linkType: hard
 
-"safe-regex-test@npm:^1.0.3":
-  version: 1.0.3
-  resolution: "safe-regex-test@npm:1.0.3"
+"safe-push-apply@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "safe-push-apply@npm:1.0.0"
   dependencies:
-    call-bind: "npm:^1.0.6"
     es-errors: "npm:^1.3.0"
-    is-regex: "npm:^1.1.4"
-  checksum: 10c0/900bf7c98dc58f08d8523b7012b468e4eb757afa624f198902c0643d7008ba777b0bdc35810ba0b758671ce887617295fb742b3f3968991b178ceca54cb07603
+    isarray: "npm:^2.0.5"
+  checksum: 10c0/831f1c9aae7436429e7862c7e46f847dfe490afac20d0ee61bae06108dbf5c745a0de3568ada30ccdd3eeb0864ca8331b2eef703abd69bfea0745b21fd320750
+  languageName: node
+  linkType: hard
+
+"safe-regex-test@npm:^1.0.3, safe-regex-test@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "safe-regex-test@npm:1.1.0"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    es-errors: "npm:^1.3.0"
+    is-regex: "npm:^1.2.1"
+  checksum: 10c0/f2c25281bbe5d39cddbbce7f86fca5ea9b3ce3354ea6cd7c81c31b006a5a9fff4286acc5450a3b9122c56c33eba69c56b9131ad751457b2b4a585825e6a10665
   languageName: node
   linkType: hard
 
@@ -15549,8 +15866,8 @@ __metadata:
   linkType: hard
 
 "sass@npm:^1.62.1":
-  version: 1.83.4
-  resolution: "sass@npm:1.83.4"
+  version: 1.85.1
+  resolution: "sass@npm:1.85.1"
   dependencies:
     "@parcel/watcher": "npm:^2.4.1"
     chokidar: "npm:^4.0.0"
@@ -15561,7 +15878,7 @@ __metadata:
       optional: true
   bin:
     sass: sass.js
-  checksum: 10c0/6f27f0eebfeb50222b14baaeef548ef58a05daf8abd9797e6c499334ed7ad40541767056c8693780d06ca83d8836348ea7396a923d3be439b133507993ca78be
+  checksum: 10c0/f843aa1df1dca2f0e9cb2fb247e4939fd514ae4c182cdd1900a0622c0d71b40dfb1c4225f78b78e165a318287ca137ec597695db3e496408bd16a921a2bc2b3f
   languageName: node
   linkType: hard
 
@@ -15674,12 +15991,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3":
-  version: 7.6.3
-  resolution: "semver@npm:7.6.3"
+"semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1":
+  version: 7.7.1
+  resolution: "semver@npm:7.7.1"
   bin:
     semver: bin/semver.js
-  checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf
+  checksum: 10c0/fd603a6fb9c399c6054015433051bdbe7b99a940a8fb44b85c2b524c4004b023d7928d47cb22154f8d054ea7ee8597f586605e05b52047f048278e4ac56ae958
   languageName: node
   linkType: hard
 
@@ -15756,21 +16073,21 @@ __metadata:
   languageName: node
   linkType: hard
 
-"set-function-length@npm:^1.2.1":
-  version: 1.2.1
-  resolution: "set-function-length@npm:1.2.1"
+"set-function-length@npm:^1.2.2":
+  version: 1.2.2
+  resolution: "set-function-length@npm:1.2.2"
   dependencies:
-    define-data-property: "npm:^1.1.2"
+    define-data-property: "npm:^1.1.4"
     es-errors: "npm:^1.3.0"
     function-bind: "npm:^1.1.2"
-    get-intrinsic: "npm:^1.2.3"
+    get-intrinsic: "npm:^1.2.4"
     gopd: "npm:^1.0.1"
-    has-property-descriptors: "npm:^1.0.1"
-  checksum: 10c0/1927e296599f2c04d210c1911f1600430a5e49e04a6d8bb03dca5487b95a574da9968813a2ced9a774bd3e188d4a6208352c8f64b8d4674cdb021dca21e190ca
+    has-property-descriptors: "npm:^1.0.2"
+  checksum: 10c0/82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c
   languageName: node
   linkType: hard
 
-"set-function-name@npm:^2.0.1, set-function-name@npm:^2.0.2":
+"set-function-name@npm:^2.0.2":
   version: 2.0.2
   resolution: "set-function-name@npm:2.0.2"
   dependencies:
@@ -15782,6 +16099,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"set-proto@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "set-proto@npm:1.0.0"
+  dependencies:
+    dunder-proto: "npm:^1.0.1"
+    es-errors: "npm:^1.3.0"
+    es-object-atoms: "npm:^1.0.0"
+  checksum: 10c0/ca5c3ccbba479d07c30460e367e66337cec825560b11e8ba9c5ebe13a2a0d6021ae34eddf94ff3dfe17a3104dc1f191519cb6c48378b503e5c3f36393938776a
+  languageName: node
+  linkType: hard
+
 "set-value@npm:^2.0.0, set-value@npm:^2.0.1":
   version: 2.0.1
   resolution: "set-value@npm:2.0.1"
@@ -15875,15 +16203,51 @@ __metadata:
   languageName: node
   linkType: hard
 
-"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6":
-  version: 1.0.6
-  resolution: "side-channel@npm:1.0.6"
+"side-channel-list@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "side-channel-list@npm:1.0.0"
   dependencies:
-    call-bind: "npm:^1.0.7"
     es-errors: "npm:^1.3.0"
-    get-intrinsic: "npm:^1.2.4"
-    object-inspect: "npm:^1.13.1"
-  checksum: 10c0/d2afd163dc733cc0a39aa6f7e39bf0c436293510dbccbff446733daeaf295857dbccf94297092ec8c53e2503acac30f0b78830876f0485991d62a90e9cad305f
+    object-inspect: "npm:^1.13.3"
+  checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d
+  languageName: node
+  linkType: hard
+
+"side-channel-map@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "side-channel-map@npm:1.0.1"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    es-errors: "npm:^1.3.0"
+    get-intrinsic: "npm:^1.2.5"
+    object-inspect: "npm:^1.13.3"
+  checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672
+  languageName: node
+  linkType: hard
+
+"side-channel-weakmap@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "side-channel-weakmap@npm:1.0.2"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    es-errors: "npm:^1.3.0"
+    get-intrinsic: "npm:^1.2.5"
+    object-inspect: "npm:^1.13.3"
+    side-channel-map: "npm:^1.0.1"
+  checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185
+  languageName: node
+  linkType: hard
+
+"side-channel@npm:^1.0.6, side-channel@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "side-channel@npm:1.1.0"
+  dependencies:
+    es-errors: "npm:^1.3.0"
+    object-inspect: "npm:^1.13.3"
+    side-channel-list: "npm:^1.0.0"
+    side-channel-map: "npm:^1.0.1"
+    side-channel-weakmap: "npm:^1.0.2"
+  checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6
   languageName: node
   linkType: hard
 
@@ -16258,6 +16622,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"stable-hash@npm:^0.0.5":
+  version: 0.0.5
+  resolution: "stable-hash@npm:0.0.5"
+  checksum: 10c0/ca670cb6d172f1c834950e4ec661e2055885df32fee3ebf3647c5df94993b7c2666a5dbc1c9a62ee11fc5c24928579ec5e81bb5ad31971d355d5a341aab493b3
+  languageName: node
+  linkType: hard
+
 "stable@npm:^0.1.8":
   version: 0.1.8
   resolution: "stable@npm:0.1.8"
@@ -16437,23 +16808,24 @@ __metadata:
   languageName: node
   linkType: hard
 
-"string.prototype.matchall@npm:^4.0.11, string.prototype.matchall@npm:^4.0.6":
-  version: 4.0.11
-  resolution: "string.prototype.matchall@npm:4.0.11"
+"string.prototype.matchall@npm:^4.0.12, string.prototype.matchall@npm:^4.0.6":
+  version: 4.0.12
+  resolution: "string.prototype.matchall@npm:4.0.12"
   dependencies:
-    call-bind: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
     define-properties: "npm:^1.2.1"
-    es-abstract: "npm:^1.23.2"
+    es-abstract: "npm:^1.23.6"
     es-errors: "npm:^1.3.0"
     es-object-atoms: "npm:^1.0.0"
-    get-intrinsic: "npm:^1.2.4"
-    gopd: "npm:^1.0.1"
-    has-symbols: "npm:^1.0.3"
-    internal-slot: "npm:^1.0.7"
-    regexp.prototype.flags: "npm:^1.5.2"
+    get-intrinsic: "npm:^1.2.6"
+    gopd: "npm:^1.2.0"
+    has-symbols: "npm:^1.1.0"
+    internal-slot: "npm:^1.1.0"
+    regexp.prototype.flags: "npm:^1.5.3"
     set-function-name: "npm:^2.0.2"
-    side-channel: "npm:^1.0.6"
-  checksum: 10c0/915a2562ac9ab5e01b7be6fd8baa0b2b233a0a9aa975fcb2ec13cc26f08fb9a3e85d5abdaa533c99c6fc4c5b65b914eba3d80c4aff9792a4c9fed403f28f7d9d
+    side-channel: "npm:^1.1.0"
+  checksum: 10c0/1a53328ada73f4a77f1fdf1c79414700cf718d0a8ef6672af5603e709d26a24f2181208144aed7e858b1bcc1a0d08567a570abfb45567db4ae47637ed2c2f85c
   languageName: node
   linkType: hard
 
@@ -16467,26 +16839,30 @@ __metadata:
   languageName: node
   linkType: hard
 
-"string.prototype.trim@npm:^1.2.9":
-  version: 1.2.9
-  resolution: "string.prototype.trim@npm:1.2.9"
+"string.prototype.trim@npm:^1.2.10":
+  version: 1.2.10
+  resolution: "string.prototype.trim@npm:1.2.10"
   dependencies:
-    call-bind: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.2"
+    define-data-property: "npm:^1.1.4"
     define-properties: "npm:^1.2.1"
-    es-abstract: "npm:^1.23.0"
+    es-abstract: "npm:^1.23.5"
     es-object-atoms: "npm:^1.0.0"
-  checksum: 10c0/dcef1a0fb61d255778155006b372dff8cc6c4394bc39869117e4241f41a2c52899c0d263ffc7738a1f9e61488c490b05c0427faa15151efad721e1a9fb2663c2
+    has-property-descriptors: "npm:^1.0.2"
+  checksum: 10c0/8a8854241c4b54a948e992eb7dd6b8b3a97185112deb0037a134f5ba57541d8248dd610c966311887b6c2fd1181a3877bffb14d873ce937a344535dabcc648f8
   languageName: node
   linkType: hard
 
-"string.prototype.trimend@npm:^1.0.8":
-  version: 1.0.8
-  resolution: "string.prototype.trimend@npm:1.0.8"
+"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9":
+  version: 1.0.9
+  resolution: "string.prototype.trimend@npm:1.0.9"
   dependencies:
-    call-bind: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.2"
     define-properties: "npm:^1.2.1"
     es-object-atoms: "npm:^1.0.0"
-  checksum: 10c0/0a0b54c17c070551b38e756ae271865ac6cc5f60dabf2e7e343cceae7d9b02e1a1120a824e090e79da1b041a74464e8477e2da43e2775c85392be30a6f60963c
+  checksum: 10c0/59e1a70bf9414cb4c536a6e31bef5553c8ceb0cf44d8b4d0ed65c9653358d1c64dd0ec203b100df83d0413bbcde38b8c5d49e14bc4b86737d74adc593a0d35b6
   languageName: node
   linkType: hard
 
@@ -16934,13 +17310,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tapable@npm:^2.2.0":
-  version: 2.2.1
-  resolution: "tapable@npm:2.2.1"
-  checksum: 10c0/bc40e6efe1e554d075469cedaba69a30eeb373552aaf41caeaaa45bf56ffacc2674261b106245bd566b35d8f3329b52d838e851ee0a852120acae26e622925c9
-  languageName: node
-  linkType: hard
-
 "tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2":
   version: 6.2.0
   resolution: "tar@npm:6.2.0"
@@ -17051,13 +17420,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"text-table@npm:^0.2.0":
-  version: 0.2.0
-  resolution: "text-table@npm:0.2.0"
-  checksum: 10c0/02805740c12851ea5982686810702e2f14369a5f4c5c40a836821e3eefc65ffeec3131ba324692a37608294b0fd8c1e55a2dd571ffed4909822787668ddbee5c
-  languageName: node
-  linkType: hard
-
 "thread-stream@npm:^3.0.0":
   version: 3.0.0
   resolution: "thread-stream@npm:3.0.0"
@@ -17111,6 +17473,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tinyglobby@npm:^0.2.12":
+  version: 0.2.12
+  resolution: "tinyglobby@npm:0.2.12"
+  dependencies:
+    fdir: "npm:^6.4.3"
+    picomatch: "npm:^4.0.2"
+  checksum: 10c0/7c9be4fd3625630e262dcb19015302aad3b4ba7fc620f269313e688f2161ea8724d6cb4444baab5ef2826eb6bed72647b169a33ec8eea37501832a2526ff540f
+  languageName: node
+  linkType: hard
+
 "tldts-core@npm:^6.1.47":
   version: 6.1.47
   resolution: "tldts-core@npm:6.1.47"
@@ -17209,12 +17581,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tough-cookie@npm:^5.0.0":
-  version: 5.0.0
-  resolution: "tough-cookie@npm:5.0.0"
+"tough-cookie@npm:^5.1.1":
+  version: 5.1.2
+  resolution: "tough-cookie@npm:5.1.2"
   dependencies:
     tldts: "npm:^6.1.32"
-  checksum: 10c0/4a69c885bf6f45c5a64e60262af99e8c0d58a33bd3d0ce5da62121eeb9c00996d0128a72df8fc4614cbde59cc8b70aa3e21e4c3c98c2bbde137d7aba7fa00124
+  checksum: 10c0/5f95023a47de0f30a902bba951664b359725597d8adeabc66a0b93a931c3af801e1e697dae4b8c21a012056c0ea88bd2bf4dfe66b2adcf8e2f42cd9796fe0626
   languageName: node
   linkType: hard
 
@@ -17243,12 +17615,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tr46@npm:^5.0.0":
-  version: 5.0.0
-  resolution: "tr46@npm:5.0.0"
+"tr46@npm:^5.1.0":
+  version: 5.1.0
+  resolution: "tr46@npm:5.1.0"
   dependencies:
     punycode: "npm:^2.3.1"
-  checksum: 10c0/1521b6e7bbc8adc825c4561480f9fe48eb2276c81335eed9fa610aa4c44a48a3221f78b10e5f18b875769eb3413e30efbf209ed556a17a42aa8d690df44b7bee
+  checksum: 10c0/d761f7144e0cb296187674ef245c74f761e334d7cf25ca73ef60e4c72c097c75051031c093fa1a2fee04b904977b316716a7915854bcae8fb1a371746513cbe8
   languageName: node
   linkType: hard
 
@@ -17259,12 +17631,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ts-api-utils@npm:^1.3.0":
-  version: 1.3.0
-  resolution: "ts-api-utils@npm:1.3.0"
+"ts-api-utils@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "ts-api-utils@npm:2.0.1"
   peerDependencies:
-    typescript: ">=4.2.0"
-  checksum: 10c0/f54a0ba9ed56ce66baea90a3fa087a484002e807f28a8ccb2d070c75e76bde64bd0f6dce98b3802834156306050871b67eec325cb4e918015a360a3f0868c77c
+    typescript: ">=4.8.4"
+  checksum: 10c0/23fd56a958b332cac00150a652e4c84730df30571bd2faa1ba6d7b511356d1a61656621492bb6c7f15dd6e18847a1408357a0e406671d358115369a17f5bfedd
   languageName: node
   linkType: hard
 
@@ -17280,17 +17652,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tslib@npm:2, tslib@npm:^2.0.0, tslib@npm:^2.7.0":
-  version: 2.8.0
-  resolution: "tslib@npm:2.8.0"
-  checksum: 10c0/31e4d14dc1355e9b89e4d3c893a18abb7f90b6886b089c2da91224d0a7752c79f3ddc41bc1aa0a588ac895bd97bb99c5bc2bfdb2f86de849f31caeb3ba79bbe5
-  languageName: node
-  linkType: hard
-
-"tslib@npm:^2.4.0, tslib@npm:^2.6.2":
-  version: 2.6.3
-  resolution: "tslib@npm:2.6.3"
-  checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a
+"tslib@npm:^2.0.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.8.0":
+  version: 2.8.1
+  resolution: "tslib@npm:2.8.1"
+  checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
   languageName: node
   linkType: hard
 
@@ -17343,13 +17708,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"type-fest@npm:^0.20.2":
-  version: 0.20.2
-  resolution: "type-fest@npm:0.20.2"
-  checksum: 10c0/dea9df45ea1f0aaa4e2d3bed3f9a0bfe9e5b2592bddb92eb1bf06e50bcf98dbb78189668cd8bc31a0511d3fc25539b4cd5c704497e53e93e2d40ca764b10bfc3
-  languageName: node
-  linkType: hard
-
 "type-fest@npm:^0.21.3":
   version: 0.21.3
   resolution: "type-fest@npm:0.21.3"
@@ -17367,87 +17725,122 @@ __metadata:
   languageName: node
   linkType: hard
 
-"typed-array-buffer@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "typed-array-buffer@npm:1.0.2"
+"typed-array-buffer@npm:^1.0.3":
+  version: 1.0.3
+  resolution: "typed-array-buffer@npm:1.0.3"
   dependencies:
-    call-bind: "npm:^1.0.7"
+    call-bound: "npm:^1.0.3"
     es-errors: "npm:^1.3.0"
-    is-typed-array: "npm:^1.1.13"
-  checksum: 10c0/9e043eb38e1b4df4ddf9dde1aa64919ae8bb909571c1cc4490ba777d55d23a0c74c7d73afcdd29ec98616d91bb3ae0f705fad4421ea147e1daf9528200b562da
+    is-typed-array: "npm:^1.1.14"
+  checksum: 10c0/1105071756eb248774bc71646bfe45b682efcad93b55532c6ffa4518969fb6241354e4aa62af679ae83899ec296d69ef88f1f3763657cdb3a4d29321f7b83079
   languageName: node
   linkType: hard
 
-"typed-array-byte-length@npm:^1.0.1":
-  version: 1.0.1
-  resolution: "typed-array-byte-length@npm:1.0.1"
+"typed-array-byte-length@npm:^1.0.3":
+  version: 1.0.3
+  resolution: "typed-array-byte-length@npm:1.0.3"
   dependencies:
-    call-bind: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
     for-each: "npm:^0.3.3"
-    gopd: "npm:^1.0.1"
-    has-proto: "npm:^1.0.3"
-    is-typed-array: "npm:^1.1.13"
-  checksum: 10c0/fcebeffb2436c9f355e91bd19e2368273b88c11d1acc0948a2a306792f1ab672bce4cfe524ab9f51a0505c9d7cd1c98eff4235c4f6bfef6a198f6cfc4ff3d4f3
+    gopd: "npm:^1.2.0"
+    has-proto: "npm:^1.2.0"
+    is-typed-array: "npm:^1.1.14"
+  checksum: 10c0/6ae083c6f0354f1fce18b90b243343b9982affd8d839c57bbd2c174a5d5dc71be9eb7019ffd12628a96a4815e7afa85d718d6f1e758615151d5f35df841ffb3e
   languageName: node
   linkType: hard
 
-"typed-array-byte-offset@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "typed-array-byte-offset@npm:1.0.2"
+"typed-array-byte-offset@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "typed-array-byte-offset@npm:1.0.4"
   dependencies:
     available-typed-arrays: "npm:^1.0.7"
-    call-bind: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
     for-each: "npm:^0.3.3"
-    gopd: "npm:^1.0.1"
-    has-proto: "npm:^1.0.3"
-    is-typed-array: "npm:^1.1.13"
-  checksum: 10c0/d2628bc739732072e39269389a758025f75339de2ed40c4f91357023c5512d237f255b633e3106c461ced41907c1bf9a533c7e8578066b0163690ca8bc61b22f
+    gopd: "npm:^1.2.0"
+    has-proto: "npm:^1.2.0"
+    is-typed-array: "npm:^1.1.15"
+    reflect.getprototypeof: "npm:^1.0.9"
+  checksum: 10c0/3d805b050c0c33b51719ee52de17c1cd8e6a571abdf0fffb110e45e8dd87a657e8b56eee94b776b13006d3d347a0c18a730b903cf05293ab6d92e99ff8f77e53
   languageName: node
   linkType: hard
 
-"typed-array-length@npm:^1.0.6":
-  version: 1.0.6
-  resolution: "typed-array-length@npm:1.0.6"
+"typed-array-length@npm:^1.0.7":
+  version: 1.0.7
+  resolution: "typed-array-length@npm:1.0.7"
   dependencies:
     call-bind: "npm:^1.0.7"
     for-each: "npm:^0.3.3"
     gopd: "npm:^1.0.1"
-    has-proto: "npm:^1.0.3"
     is-typed-array: "npm:^1.1.13"
     possible-typed-array-names: "npm:^1.0.0"
-  checksum: 10c0/74253d7dc488eb28b6b2711cf31f5a9dcefc9c41b0681fd1c178ed0a1681b4468581a3626d39cd4df7aee3d3927ab62be06aa9ca74e5baf81827f61641445b77
+    reflect.getprototypeof: "npm:^1.0.6"
+  checksum: 10c0/e38f2ae3779584c138a2d8adfa8ecf749f494af3cd3cdafe4e688ce51418c7d2c5c88df1bd6be2bbea099c3f7cea58c02ca02ed438119e91f162a9de23f61295
   languageName: node
   linkType: hard
 
-"typescript@npm:5, typescript@npm:^5.0.4":
-  version: 5.7.2
-  resolution: "typescript@npm:5.7.2"
-  bin:
-    tsc: bin/tsc
-    tsserver: bin/tsserver
-  checksum: 10c0/a873118b5201b2ef332127ef5c63fb9d9c155e6fdbe211cbd9d8e65877283797cca76546bad742eea36ed7efbe3424a30376818f79c7318512064e8625d61622
-  languageName: node
-  linkType: hard
-
-"typescript@patch:typescript@npm%3A5#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.0.4#optional!builtin<compat/typescript>":
-  version: 5.7.2
-  resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin<compat/typescript>::version=5.7.2&hash=5786d5"
-  bin:
-    tsc: bin/tsc
-    tsserver: bin/tsserver
-  checksum: 10c0/f3b8082c9d1d1629a215245c9087df56cb784f9fb6f27b5d55577a20e68afe2a889c040aacff6d27e35be165ecf9dca66e694c42eb9a50b3b2c451b36b5675cb
-  languageName: node
-  linkType: hard
-
-"unbox-primitive@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "unbox-primitive@npm:1.0.2"
+"typescript-eslint@npm:^8.28.0":
+  version: 8.28.0
+  resolution: "typescript-eslint@npm:8.28.0"
   dependencies:
-    call-bind: "npm:^1.0.2"
+    "@typescript-eslint/eslint-plugin": "npm:8.28.0"
+    "@typescript-eslint/parser": "npm:8.28.0"
+    "@typescript-eslint/utils": "npm:8.28.0"
+  peerDependencies:
+    eslint: ^8.57.0 || ^9.0.0
+    typescript: ">=4.8.4 <5.9.0"
+  checksum: 10c0/bf1c1e4b2f21a95930758d5b285c39a394a50e3b6983f373413b93b80a6cb5aabc1d741780e60c63cb42ad5d645ea9c1e6d441d98174c5a2884ab88f4ac46df6
+  languageName: node
+  linkType: hard
+
+"typescript@npm:^5.6.0":
+  version: 5.8.2
+  resolution: "typescript@npm:5.8.2"
+  bin:
+    tsc: bin/tsc
+    tsserver: bin/tsserver
+  checksum: 10c0/5c4f6fbf1c6389b6928fe7b8fcd5dc73bb2d58cd4e3883f1d774ed5bd83b151cbac6b7ecf11723de56d4676daeba8713894b1e9af56174f2f9780ae7848ec3c6
+  languageName: node
+  linkType: hard
+
+"typescript@npm:~5.7.3":
+  version: 5.7.3
+  resolution: "typescript@npm:5.7.3"
+  bin:
+    tsc: bin/tsc
+    tsserver: bin/tsserver
+  checksum: 10c0/b7580d716cf1824736cc6e628ab4cd8b51877408ba2be0869d2866da35ef8366dd6ae9eb9d0851470a39be17cbd61df1126f9e211d8799d764ea7431d5435afa
+  languageName: node
+  linkType: hard
+
+"typescript@patch:typescript@npm%3A^5.6.0#optional!builtin<compat/typescript>":
+  version: 5.8.2
+  resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin<compat/typescript>::version=5.8.2&hash=5786d5"
+  bin:
+    tsc: bin/tsc
+    tsserver: bin/tsserver
+  checksum: 10c0/5448a08e595cc558ab321e49d4cac64fb43d1fa106584f6ff9a8d8e592111b373a995a1d5c7f3046211c8a37201eb6d0f1566f15cdb7a62a5e3be01d087848e2
+  languageName: node
+  linkType: hard
+
+"typescript@patch:typescript@npm%3A~5.7.3#optional!builtin<compat/typescript>":
+  version: 5.7.3
+  resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin<compat/typescript>::version=5.7.3&hash=5786d5"
+  bin:
+    tsc: bin/tsc
+    tsserver: bin/tsserver
+  checksum: 10c0/6fd7e0ed3bf23a81246878c613423730c40e8bdbfec4c6e4d7bf1b847cbb39076e56ad5f50aa9d7ebd89877999abaee216002d3f2818885e41c907caaa192cc4
+  languageName: node
+  linkType: hard
+
+"unbox-primitive@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "unbox-primitive@npm:1.1.0"
+  dependencies:
+    call-bound: "npm:^1.0.3"
     has-bigints: "npm:^1.0.2"
-    has-symbols: "npm:^1.0.3"
-    which-boxed-primitive: "npm:^1.0.2"
-  checksum: 10c0/81ca2e81134167cc8f75fa79fbcc8a94379d6c61de67090986a2273850989dd3bae8440c163121b77434b68263e34787a675cbdcb34bb2f764c6b9c843a11b66
+    has-symbols: "npm:^1.1.0"
+    which-boxed-primitive: "npm:^1.1.1"
+  checksum: 10c0/7dbd35ab02b0e05fe07136c72cb9355091242455473ec15057c11430129bab38b7b3624019b8778d02a881c13de44d63cd02d122ee782fb519e1de7775b5b982
   languageName: node
   linkType: hard
 
@@ -17465,17 +17858,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"undici-types@npm:~5.26.4":
-  version: 5.26.5
-  resolution: "undici-types@npm:5.26.5"
-  checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501
-  languageName: node
-  linkType: hard
-
-"undici-types@npm:~6.19.8":
-  version: 6.19.8
-  resolution: "undici-types@npm:6.19.8"
-  checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344
+"undici-types@npm:~6.20.0":
+  version: 6.20.0
+  resolution: "undici-types@npm:6.20.0"
+  checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf
   languageName: node
   linkType: hard
 
@@ -17604,6 +17990,60 @@ __metadata:
   languageName: node
   linkType: hard
 
+"unrs-resolver@npm:^1.3.2":
+  version: 1.3.2
+  resolution: "unrs-resolver@npm:1.3.2"
+  dependencies:
+    "@unrs/resolver-binding-darwin-arm64": "npm:1.3.2"
+    "@unrs/resolver-binding-darwin-x64": "npm:1.3.2"
+    "@unrs/resolver-binding-freebsd-x64": "npm:1.3.2"
+    "@unrs/resolver-binding-linux-arm-gnueabihf": "npm:1.3.2"
+    "@unrs/resolver-binding-linux-arm-musleabihf": "npm:1.3.2"
+    "@unrs/resolver-binding-linux-arm64-gnu": "npm:1.3.2"
+    "@unrs/resolver-binding-linux-arm64-musl": "npm:1.3.2"
+    "@unrs/resolver-binding-linux-ppc64-gnu": "npm:1.3.2"
+    "@unrs/resolver-binding-linux-s390x-gnu": "npm:1.3.2"
+    "@unrs/resolver-binding-linux-x64-gnu": "npm:1.3.2"
+    "@unrs/resolver-binding-linux-x64-musl": "npm:1.3.2"
+    "@unrs/resolver-binding-wasm32-wasi": "npm:1.3.2"
+    "@unrs/resolver-binding-win32-arm64-msvc": "npm:1.3.2"
+    "@unrs/resolver-binding-win32-ia32-msvc": "npm:1.3.2"
+    "@unrs/resolver-binding-win32-x64-msvc": "npm:1.3.2"
+  dependenciesMeta:
+    "@unrs/resolver-binding-darwin-arm64":
+      optional: true
+    "@unrs/resolver-binding-darwin-x64":
+      optional: true
+    "@unrs/resolver-binding-freebsd-x64":
+      optional: true
+    "@unrs/resolver-binding-linux-arm-gnueabihf":
+      optional: true
+    "@unrs/resolver-binding-linux-arm-musleabihf":
+      optional: true
+    "@unrs/resolver-binding-linux-arm64-gnu":
+      optional: true
+    "@unrs/resolver-binding-linux-arm64-musl":
+      optional: true
+    "@unrs/resolver-binding-linux-ppc64-gnu":
+      optional: true
+    "@unrs/resolver-binding-linux-s390x-gnu":
+      optional: true
+    "@unrs/resolver-binding-linux-x64-gnu":
+      optional: true
+    "@unrs/resolver-binding-linux-x64-musl":
+      optional: true
+    "@unrs/resolver-binding-wasm32-wasi":
+      optional: true
+    "@unrs/resolver-binding-win32-arm64-msvc":
+      optional: true
+    "@unrs/resolver-binding-win32-ia32-msvc":
+      optional: true
+    "@unrs/resolver-binding-win32-x64-msvc":
+      optional: true
+  checksum: 10c0/f9b6d18193bcaae7ef9e284a74c85d4cb3d8c833851f1b23254a947297e672826223d82798dbff818455fefeda02084340aca904300fd5060468c2f243767cc1
+  languageName: node
+  linkType: hard
+
 "unset-value@npm:^1.0.0":
   version: 1.0.0
   resolution: "unset-value@npm:1.0.0"
@@ -17621,17 +18061,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"update-browserslist-db@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "update-browserslist-db@npm:1.1.0"
+"update-browserslist-db@npm:^1.1.1":
+  version: 1.1.2
+  resolution: "update-browserslist-db@npm:1.1.2"
   dependencies:
-    escalade: "npm:^3.1.2"
-    picocolors: "npm:^1.0.1"
+    escalade: "npm:^3.2.0"
+    picocolors: "npm:^1.1.1"
   peerDependencies:
     browserslist: ">= 4.21.0"
   bin:
     update-browserslist-db: cli.js
-  checksum: 10c0/a7452de47785842736fb71547651c5bbe5b4dc1e3722ccf48a704b7b34e4dcf633991eaa8e4a6a517ffb738b3252eede3773bef673ef9021baa26b056d63a5b9
+  checksum: 10c0/9cb353998d6d7d6ba1e46b8fa3db888822dd972212da4eda609d185eb5c3557a93fd59780ceb757afd4d84240518df08542736969e6a5d6d6ce2d58e9363aac6
   languageName: node
   linkType: hard
 
@@ -17795,11 +18235,11 @@ __metadata:
   linkType: hard
 
 "uuid@npm:^11.0.0":
-  version: 11.0.5
-  resolution: "uuid@npm:11.0.5"
+  version: 11.1.0
+  resolution: "uuid@npm:11.1.0"
   bin:
     uuid: dist/esm/bin/uuid
-  checksum: 10c0/6f59f0c605e02c14515401084ca124b9cb462b4dcac866916a49862bcf831874508a308588c23a7718269226ad11a92da29b39d761ad2b86e736623e3a33b6e7
+  checksum: 10c0/34aa51b9874ae398c2b799c88a127701408cd581ee89ec3baa53509dd8728cbb25826f2a038f9465f8b7be446f0fbf11558862965b18d21c993684297628d4d3
   languageName: node
   linkType: hard
 
@@ -18224,13 +18664,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.0":
-  version: 14.1.0
-  resolution: "whatwg-url@npm:14.1.0"
+"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.1":
+  version: 14.2.0
+  resolution: "whatwg-url@npm:14.2.0"
   dependencies:
-    tr46: "npm:^5.0.0"
+    tr46: "npm:^5.1.0"
     webidl-conversions: "npm:^7.0.0"
-  checksum: 10c0/f00104f1c67ce086ba8ffedab529cbbd9aefd8c0a6555320026de7aeff31f91c38680f95818b140a7c9cc657cde3781e567835dda552ddb1e2b8faaba0ac3cb6
+  checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272
   languageName: node
   linkType: hard
 
@@ -18255,48 +18695,49 @@ __metadata:
   languageName: node
   linkType: hard
 
-"which-boxed-primitive@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "which-boxed-primitive@npm:1.0.2"
+"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "which-boxed-primitive@npm:1.1.1"
   dependencies:
-    is-bigint: "npm:^1.0.1"
-    is-boolean-object: "npm:^1.1.0"
-    is-number-object: "npm:^1.0.4"
-    is-string: "npm:^1.0.5"
-    is-symbol: "npm:^1.0.3"
-  checksum: 10c0/0a62a03c00c91dd4fb1035b2f0733c341d805753b027eebd3a304b9cb70e8ce33e25317add2fe9b5fea6f53a175c0633ae701ff812e604410ddd049777cd435e
+    is-bigint: "npm:^1.1.0"
+    is-boolean-object: "npm:^1.2.1"
+    is-number-object: "npm:^1.1.1"
+    is-string: "npm:^1.1.1"
+    is-symbol: "npm:^1.1.1"
+  checksum: 10c0/aceea8ede3b08dede7dce168f3883323f7c62272b49801716e8332ff750e7ae59a511ae088840bc6874f16c1b7fd296c05c949b0e5b357bfe3c431b98c417abe
   languageName: node
   linkType: hard
 
-"which-builtin-type@npm:^1.1.3":
-  version: 1.1.3
-  resolution: "which-builtin-type@npm:1.1.3"
+"which-builtin-type@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "which-builtin-type@npm:1.2.1"
   dependencies:
-    function.prototype.name: "npm:^1.1.5"
-    has-tostringtag: "npm:^1.0.0"
+    call-bound: "npm:^1.0.2"
+    function.prototype.name: "npm:^1.1.6"
+    has-tostringtag: "npm:^1.0.2"
     is-async-function: "npm:^2.0.0"
-    is-date-object: "npm:^1.0.5"
-    is-finalizationregistry: "npm:^1.0.2"
+    is-date-object: "npm:^1.1.0"
+    is-finalizationregistry: "npm:^1.1.0"
     is-generator-function: "npm:^1.0.10"
-    is-regex: "npm:^1.1.4"
+    is-regex: "npm:^1.2.1"
     is-weakref: "npm:^1.0.2"
     isarray: "npm:^2.0.5"
-    which-boxed-primitive: "npm:^1.0.2"
-    which-collection: "npm:^1.0.1"
-    which-typed-array: "npm:^1.1.9"
-  checksum: 10c0/2b7b234df3443b52f4fbd2b65b731804de8d30bcc4210ec84107ef377a81923cea7f2763b7fb78b394175cea59118bf3c41b9ffd2d643cb1d748ef93b33b6bd4
+    which-boxed-primitive: "npm:^1.1.0"
+    which-collection: "npm:^1.0.2"
+    which-typed-array: "npm:^1.1.16"
+  checksum: 10c0/8dcf323c45e5c27887800df42fbe0431d0b66b1163849bb7d46b5a730ad6a96ee8bfe827d078303f825537844ebf20c02459de41239a0a9805e2fcb3cae0d471
   languageName: node
   linkType: hard
 
-"which-collection@npm:^1.0.1":
-  version: 1.0.1
-  resolution: "which-collection@npm:1.0.1"
+"which-collection@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "which-collection@npm:1.0.2"
   dependencies:
-    is-map: "npm:^2.0.1"
-    is-set: "npm:^2.0.1"
-    is-weakmap: "npm:^2.0.1"
-    is-weakset: "npm:^2.0.1"
-  checksum: 10c0/249f913e1758ed2f06f00706007d87dc22090a80591a56917376e70ecf8fc9ab6c41d98e1c87208bb9648676f65d4b09c0e4d23c56c7afb0f0a73a27d701df5d
+    is-map: "npm:^2.0.3"
+    is-set: "npm:^2.0.3"
+    is-weakmap: "npm:^2.0.2"
+    is-weakset: "npm:^2.0.3"
+  checksum: 10c0/3345fde20964525a04cdf7c4a96821f85f0cc198f1b2ecb4576e08096746d129eb133571998fe121c77782ac8f21cbd67745a3d35ce100d26d4e684c142ea1f2
   languageName: node
   linkType: hard
 
@@ -18307,16 +18748,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.9":
-  version: 1.1.15
-  resolution: "which-typed-array@npm:1.1.15"
+"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18":
+  version: 1.1.18
+  resolution: "which-typed-array@npm:1.1.18"
   dependencies:
     available-typed-arrays: "npm:^1.0.7"
-    call-bind: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
     for-each: "npm:^0.3.3"
-    gopd: "npm:^1.0.1"
+    gopd: "npm:^1.2.0"
     has-tostringtag: "npm:^1.0.2"
-  checksum: 10c0/4465d5348c044032032251be54d8988270e69c6b7154f8fcb2a47ff706fe36f7624b3a24246b8d9089435a8f4ec48c1c1025c5d6b499456b9e5eff4f48212983
+  checksum: 10c0/0412f4a91880ca1a2a63056187c2e3de6b129b2b5b6c17bc3729f0f7041047ae48fb7424813e51506addb2c97320003ee18b8c57469d2cde37983ef62126143c
   languageName: node
   linkType: hard
 
@@ -18668,8 +19110,8 @@ __metadata:
   linkType: hard
 
 "ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.18.0":
-  version: 8.18.0
-  resolution: "ws@npm:8.18.0"
+  version: 8.18.1
+  resolution: "ws@npm:8.18.1"
   peerDependencies:
     bufferutil: ^4.0.1
     utf-8-validate: ">=5.0.2"
@@ -18678,7 +19120,7 @@ __metadata:
       optional: true
     utf-8-validate:
       optional: true
-  checksum: 10c0/25eb33aff17edcb90721ed6b0eb250976328533ad3cd1a28a274bd263682e7296a6591ff1436d6cbc50fa67463158b062f9d1122013b361cec99a05f84680e06
+  checksum: 10c0/e498965d6938c63058c4310ffb6967f07d4fa06789d3364829028af380d299fe05762961742971c764973dce3d1f6a2633fe8b2d9410c9b52e534b4b882a99fa
   languageName: node
   linkType: hard