Repository: saleor/saleor
Pull request: Query object translation with only manage_translation permission
I want to merge this change because resolve #4834 

<!-- Please mention all relevant issue numbers. -->

### Screenshots

<!-- If your changes affect the UI, providing "before" and "after" screenshots will
greatly reduce the amount of work needed to review your work. -->

### Pull Request Checklist

<!-- Please keep this section. It will make maintainer's life easier. -->

1. [ ] Privileged views and APIs are guarded by proper permission checks.
1. [ ] All visible strings are translated with proper context.
1. [ ] All data-formatting is locale-aware (dates, numbers, and so on).
1. [ ] Database queries are optimized and the number of queries is constant.
1. [ ] Database migration files are up to date.
1. [ ] The changes are tested.
1. [ ] GraphQL schema and type definitions are up to date.
1. [ ] Changes are mentioned in the changelog.


Problem statement:
Translate eg. products without manage_products permissions
### What I'm trying to achieve
To translate hidden products, sales and other permission-protected objects.

### Describe a proposed solution
```
translation(kind: PRODUCT, id: "", languageCode: DE) {
  ..on Product {
    id
    name
    description
    ...
    translation(languageCode: $languageCode) {
      id
      name
      description
      ...
    }
  }
}
```

Hints from issue discussion:

Commit message: Fix review remarks

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79ffa42fd26..f80b101e4b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Add sub-categories to random data - #4949 by @IKarbowiak
 - Deprecate `localized` field in Money type - #4952 by @IKarbowiak
 - Fix for shipping api doesn't apply taxes - #4913 by @kswiatek92
+- Query object translation with only manage_translation permission - #4914 by @fowczarek
 
 ## 2.9.0
 
diff --git a/saleor/graphql/page/resolvers.py b/saleor/graphql/page/resolvers.py
index fd4e4a5a273..c3112454ef1 100644
--- a/saleor/graphql/page/resolvers.py
+++ b/saleor/graphql/page/resolvers.py
@@ -2,29 +2,19 @@
 
 from ...page import models
 from ..utils import filter_by_query_param
-from .types import Page
 
 PAGE_SEARCH_FIELDS = ("content", "slug", "title")
 
 
-def resolve_page(info, page_id=None, slug=None):
-    assert page_id or slug, "No page ID or slug provided."
+def resolve_page(info, global_page_id=None, slug=None):
+    assert global_page_id or slug, "No page ID or slug provided."
     user = info.context.user
 
     if slug is not None:
-        try:
-            page = models.Page.objects.visible_to_user(user).get(slug=slug)
-        except models.Page.DoesNotExist:
-            page = None
+        page = models.Page.objects.visible_to_user(user).filter(slug=slug).first()
     else:
-        page = graphene.Node.get_node_from_global_id(info, page_id, Page)
-        # Resolve to null if page is not published and user has no permission
-        # to manage pages.
-        is_available_to_user = (
-            page and page.is_published or user.has_perm("page.manage_pages")
-        )
-        if not is_available_to_user:
-            page = None
+        _type, page_pk = graphene.Node.from_global_id(global_page_id)
+        page = models.Page.objects.visible_to_user(user).filter(pk=page_pk).first()
     return page
 
 
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index 67503ff6002..7a8d1c4e063 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -322,6 +322,13 @@ input AttributeSortingInput {
   direction: OrderDirection!
 }
 
+type AttributeTranslatableContent implements Node {
+  id: ID!
+  name: String!
+  translation(languageCode: LanguageCodeEnum!): AttributeTranslation
+  attribute: Attribute
+}
+
 type AttributeTranslate {
   errors: [Error!]
   attribute: Attribute
@@ -414,6 +421,13 @@ input AttributeValueInput {
   values: [String]!
 }
 
+type AttributeValueTranslatableContent implements Node {
+  id: ID!
+  name: String!
+  translation(languageCode: LanguageCodeEnum!): AttributeValueTranslation
+  attributeValue: AttributeValue
+}
+
 type AttributeValueTranslate {
   errors: [Error!]
   attributeValue: AttributeValue
@@ -486,9 +500,9 @@ type Category implements Node {
   seoDescription: String
   id: ID!
   name: String!
-  slug: String!
   description: String!
   descriptionJson: JSONString!
+  slug: String!
   parent: Category
   level: Int!
   privateMeta: [MetaStore]!
@@ -556,6 +570,17 @@ input CategoryInput {
   backgroundImageAlt: String
 }
 
+type CategoryTranslatableContent implements Node {
+  seoTitle: String
+  seoDescription: String
+  id: ID!
+  name: String!
+  description: String!
+  descriptionJson: JSONString!
+  translation(languageCode: LanguageCodeEnum!): CategoryTranslation
+  category: Category
+}
+
 type CategoryTranslate {
   errors: [Error!]
   category: Category
@@ -809,15 +834,15 @@ type ChoiceValue {
 }
 
 type Collection implements Node {
-  publicationDate: Date
-  isPublished: Boolean!
   seoTitle: String
   seoDescription: String
   id: ID!
   name: String!
-  slug: String!
   description: String!
   descriptionJson: JSONString!
+  publicationDate: Date
+  isPublished: Boolean!
+  slug: String!
   privateMeta: [MetaStore]!
   meta: [MetaStore]!
   products(before: String, after: String, first: Int, last: Int): ProductCountableConnection
@@ -925,6 +950,17 @@ type CollectionReorderProducts {
   productErrors: [ProductError!]
 }
 
+type CollectionTranslatableContent implements Node {
+  seoTitle: String
+  seoDescription: String
+  id: ID!
+  name: String!
+  description: String!
+  descriptionJson: JSONString!
+  translation(languageCode: LanguageCodeEnum!): CollectionTranslation
+  collection: Collection
+}
+
 type CollectionTranslate {
   errors: [Error!]
   collection: Collection
@@ -1362,6 +1398,8 @@ input DateTimeRangeInput {
 
 scalar Decimal
 
+union DefaultTranslationItem = ProductTranslatableContent | CollectionTranslatableContent | CategoryTranslatableContent | AttributeTranslatableContent | AttributeValueTranslatableContent | ProductVariantTranslatableContent | PageTranslatableContent | ShippingMethodTranslatableContent | SaleTranslatableContent | VoucherTranslatableContent | MenuItemTranslatableContent
+
 type DigitalContent implements Node {
   useDefaultSettings: Boolean!
   automaticFulfillment: Boolean!
@@ -1876,9 +1914,9 @@ input MenuInput {
 
 type MenuItem implements Node {
   id: ID!
+  name: String!
   sortOrder: Int @deprecated(reason: "Will be dropped in 2.10 and is deprecated since 2.9: use the position in list instead and relative calculus to determine shift position.")
   menu: Menu!
-  name: String!
   parent: MenuItem
   category: Category
   collection: Collection
@@ -1952,6 +1990,13 @@ input MenuItemMoveInput {
   sortOrder: Int
 }
 
+type MenuItemTranslatableContent implements Node {
+  id: ID!
+  name: String!
+  translation(languageCode: LanguageCodeEnum!): MenuItemTranslation
+  menuItem: MenuItem
+}
+
 type MenuItemTranslate {
   errors: [Error!]
   menuItem: MenuItem
@@ -2586,15 +2631,15 @@ type OrderVoid {
 }
 
 type Page implements Node {
-  publicationDate: Date
-  isPublished: Boolean!
   seoTitle: String
   seoDescription: String
   id: ID!
-  slug: String!
   title: String!
   content: String!
   contentJson: JSONString!
+  publicationDate: Date
+  isPublished: Boolean!
+  slug: String!
   created: DateTime!
   translation(languageCode: LanguageCodeEnum!): PageTranslation
 }
@@ -2651,9 +2696,20 @@ input PageInput {
   seo: SeoInput
 }
 
+type PageTranslatableContent implements Node {
+  seoTitle: String
+  seoDescription: String
+  id: ID!
+  title: String!
+  content: String!
+  contentJson: JSONString!
+  translation(languageCode: LanguageCodeEnum!): PageTranslation
+  page: Page
+}
+
 type PageTranslate {
   errors: [Error!]
-  page: Page
+  page: PageTranslatableContent
 }
 
 type PageTranslation implements Node {
@@ -2854,14 +2910,14 @@ input PriceRangeInput {
 
 type Product implements Node {
   id: ID!
-  publicationDate: Date
-  isPublished: Boolean!
   seoTitle: String
   seoDescription: String
-  productType: ProductType!
   name: String!
   description: String!
   descriptionJson: JSONString!
+  publicationDate: Date
+  isPublished: Boolean!
+  productType: ProductType!
   category: Category!
   updatedAt: DateTime
   chargeTaxes: Boolean!
@@ -3082,6 +3138,17 @@ type ProductPricingInfo {
   priceRangeLocalCurrency: TaxedMoneyRange
 }
 
+type ProductTranslatableContent implements Node {
+  id: ID!
+  seoTitle: String
+  seoDescription: String
+  name: String!
+  description: String!
+  descriptionJson: JSONString!
+  translation(languageCode: LanguageCodeEnum!): ProductTranslation
+  product: Product
+}
+
 type ProductTranslate {
   errors: [Error!]
   product: Product
@@ -3226,8 +3293,8 @@ type ProductUpdatePrivateMeta {
 
 type ProductVariant implements Node {
   id: ID!
-  sku: String!
   name: String!
+  sku: String!
   product: Product!
   trackInventory: Boolean!
   quantityAllocated: Int!
@@ -3330,6 +3397,13 @@ input ProductVariantInput {
   weight: WeightScalar
 }
 
+type ProductVariantTranslatableContent implements Node {
+  id: ID!
+  name: String!
+  translation(languageCode: LanguageCodeEnum!): ProductVariantTranslation
+  productVariant: ProductVariant
+}
+
 type ProductVariantTranslate {
   errors: [Error!]
   productVariant: ProductVariant
@@ -3365,6 +3439,7 @@ type Query {
   webhookEvents: [WebhookEvent]
   webhookSamplePayload(eventType: WebhookEventTypeEnum!): JSONString
   translations(kind: TranslatableKinds!, before: String, after: String, first: Int, last: Int): TranslatableItemConnection
+  translation(id: ID!, kind: TranslatableKinds!): DefaultTranslationItem
   shop: Shop
   shippingZone(id: ID!): ShippingZone
   shippingZones(before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection
@@ -3515,6 +3590,13 @@ type SaleRemoveCatalogues {
   sale: Sale
 }
 
+type SaleTranslatableContent implements Node {
+  id: ID!
+  name: String!
+  translation(languageCode: LanguageCodeEnum!): SaleTranslation
+  sale: Sale
+}
+
 type SaleTranslate {
   errors: [Error!]
   sale: Sale
@@ -3670,6 +3752,13 @@ type ShippingMethod implements Node {
   translation(languageCode: LanguageCodeEnum!): ShippingMethodTranslation
 }
 
+type ShippingMethodTranslatableContent implements Node {
+  id: ID!
+  name: String!
+  translation(languageCode: LanguageCodeEnum!): ShippingMethodTranslation
+  shippingMethod: ShippingMethod
+}
+
 type ShippingMethodTranslation implements Node {
   id: ID!
   name: String
@@ -4190,8 +4279,8 @@ type VerifyToken {
 
 type Voucher implements Node {
   id: ID!
-  type: VoucherTypeEnum!
   name: String
+  type: VoucherTypeEnum!
   code: String!
   usageLimit: Int
   used: Int!
@@ -4280,6 +4369,13 @@ type VoucherRemoveCatalogues {
   voucher: Voucher
 }
 
+type VoucherTranslatableContent implements Node {
+  id: ID!
+  name: String
+  translation(languageCode: LanguageCodeEnum!): VoucherTranslation
+  voucher: Voucher
+}
+
 type VoucherTranslate {
   errors: [Error!]
   voucher: Voucher
diff --git a/saleor/graphql/translations/schema.py b/saleor/graphql/translations/schema.py
index 37c49a85cb8..2382b8031e5 100644
--- a/saleor/graphql/translations/schema.py
+++ b/saleor/graphql/translations/schema.py
@@ -1,7 +1,20 @@
 import graphene
 
+from ...discount.models import Sale, Voucher
+from ...menu.models import MenuItem
+from ...page.models import Page
+from ...product.models import (
+    Attribute,
+    AttributeValue,
+    Category,
+    Collection,
+    Product,
+    ProductVariant,
+)
+from ...shipping.models import ShippingMethod
 from ..core.connection import CountableConnection
 from ..core.fields import BaseConnectionField
+from ..decorators import permission_required
 from ..discount import types as discount_types
 from ..discount.resolvers import resolve_sales, resolve_vouchers
 from ..menu import types as menu_types
@@ -17,6 +30,7 @@
     resolve_products,
 )
 from ..shipping import types as shipping_types
+from ..translations import types as translation_types
 from .resolvers import resolve_attribute_values, resolve_shipping_methods
 
 
@@ -37,6 +51,25 @@ class Meta:
         )
 
 
+# TODO Consider name of this class, we should replace to TranslatableItem after
+# `translations` query refactor. Issue #4957
+class DefaultTranslationItem(graphene.Union):
+    class Meta:
+        types = (
+            translation_types.ProductTranslatableContent,
+            translation_types.CollectionTranslatableContent,
+            translation_types.CategoryTranslatableContent,
+            translation_types.AttributeTranslatableContent,
+            translation_types.AttributeValueTranslatableContent,
+            translation_types.ProductVariantTranslatableContent,
+            translation_types.PageTranslatableContent,
+            translation_types.ShippingMethodTranslatableContent,
+            translation_types.SaleTranslatableContent,
+            translation_types.VoucherTranslatableContent,
+            translation_types.MenuItemTranslatableContent,
+        )
+
+
 class TranslatableItemConnection(CountableConnection):
     class Meta:
         node = TranslatableItem
@@ -44,19 +77,20 @@ class Meta:
 
 class TranslatableKinds(graphene.Enum):
     ATTRIBUTE = "Attribute"
-    ATTRIBUTE_VALUE = "Attribute value"
+    ATTRIBUTE_VALUE = "AttributeValue"
     CATEGORY = "Category"
     COLLECTION = "Collection"
-    MENU_ITEM = "Menu item"
+    MENU_ITEM = "MenuItem"
     PAGE = "Page"
     PRODUCT = "Product"
     SALE = "Sale"
-    SHIPPING_METHOD = "Shipping method"
-    VARIANT = "Variant"
+    SHIPPING_METHOD = "ShippingMethod"
+    VARIANT = "ProductVariant"
     VOUCHER = "Voucher"
 
 
 class TranslationQueries(graphene.ObjectType):
+    # TODO We nead to change output of this query to new types. Issue #4957
     translations = BaseConnectionField(
         TranslatableItemConnection,
         description="Returns a list of all translatable items of a given kind.",
@@ -64,6 +98,15 @@ class TranslationQueries(graphene.ObjectType):
             TranslatableKinds, required=True, description="Kind of objects to retrieve."
         ),
     )
+    translation = graphene.Field(
+        DefaultTranslationItem,
+        id=graphene.Argument(
+            graphene.ID, description="ID of the objects to retrieve.", required=True
+        ),
+        kind=graphene.Argument(
+            TranslatableKinds, required=True, description="Kind of objects to retrieve."
+        ),
+    )
 
     def resolve_translations(self, info, kind, **_kwargs):
         if kind == TranslatableKinds.PRODUCT:
@@ -88,3 +131,25 @@ def resolve_translations(self, info, kind, **_kwargs):
             return resolve_menu_items(info, query=None)
         elif kind == TranslatableKinds.SALE:
             return resolve_sales(info, query=None)
+
+    @permission_required("site.manage_translations")
+    def resolve_translation(self, info, id, kind, **_kwargs):
+        # Disable all the no-member violations in this function
+        # pylint: disable=no-member
+        _type, kind_id = graphene.Node.from_global_id(id)
+        if not _type == kind:
+            return None
+        models = {
+            TranslatableKinds.PRODUCT.value: Product,
+            TranslatableKinds.COLLECTION.value: Collection,
+            TranslatableKinds.CATEGORY.value: Category,
+            TranslatableKinds.ATTRIBUTE.value: Attribute,
+            TranslatableKinds.ATTRIBUTE_VALUE.value: AttributeValue,
+            TranslatableKinds.VARIANT.value: ProductVariant,
+            TranslatableKinds.PAGE.value: Page,
+            TranslatableKinds.SHIPPING_METHOD.value: ShippingMethod,
+            TranslatableKinds.SALE.value: Sale,
+            TranslatableKinds.VOUCHER.value: Voucher,
+            TranslatableKinds.MENU_ITEM.value: MenuItem,
+        }
+        return models[kind].objects.filter(pk=kind_id).first()
diff --git a/saleor/graphql/translations/types.py b/saleor/graphql/translations/types.py
index 64aec0a2a61..3483b6af1b2 100644
--- a/saleor/graphql/translations/types.py
+++ b/saleor/graphql/translations/types.py
@@ -10,7 +10,19 @@
 from ..core.connection import CountableDjangoObjectType
 from ..core.types import LanguageDisplay
 from ..core.utils import str_to_enum
+from ..decorators import permission_required
 from .enums import LanguageCodeEnum
+from .resolvers import resolve_translation
+
+BASIC_TRANSLATABLE_FIELDS = ["id", "name"]
+EXTENDED_TRANSLATABLE_FIELDS = [
+    "id",
+    "name",
+    "description",
+    "description_json",
+    "seo_title",
+    "seo_description",
+]
 
 
 class BaseTranslationType(CountableDjangoObjectType):
@@ -40,68 +52,258 @@ class AttributeValueTranslation(BaseTranslationType):
     class Meta:
         model = product_models.AttributeValueTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = ["id", "name"]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+
+class AttributeValueTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        AttributeValueTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description=(
+            "Returns translated Attribute Value fields for the given language code."
+        ),
+        resolver=resolve_translation,
+    )
+    attribute_value = graphene.Field(
+        "saleor.graphql.product.types.attributes.AttributeValue",
+        description="Represents a value of an attribute.",
+    )
+
+    class Meta:
+        model = product_models.AttributeValue
+        interfaces = [graphene.relay.Node]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    def resolve_attribute_value(root: product_models.AttributeValue, _info):
+        return root
 
 
 class AttributeTranslation(BaseTranslationType):
     class Meta:
         model = product_models.AttributeTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = ["id", "name"]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+
+class AttributeTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        AttributeTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description=(
+            "Returns translated Attribute fields for the given language code."
+        ),
+        resolver=resolve_translation,
+    )
+    attribute = graphene.Field(
+        "saleor.graphql.product.types.attributes.Attribute",
+        description="Custom attribute of a product.",
+    )
+
+    class Meta:
+        model = product_models.Attribute
+        interfaces = [graphene.relay.Node]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    def resolve_attribute(root: product_models.Attribute, _info):
+        return root
 
 
 class ProductVariantTranslation(BaseTranslationType):
     class Meta:
         model = product_models.ProductVariantTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = ["id", "name"]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+
+class ProductVariantTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        ProductVariantTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description=(
+            "Returns translated Product Variant fields for the given language code."
+        ),
+        resolver=resolve_translation,
+    )
+    product_variant = graphene.Field(
+        "saleor.graphql.product.types.products.ProductVariant",
+        description=(
+            "Represents a version of a product such as different size or color."
+        ),
+    )
+
+    class Meta:
+        model = product_models.ProductVariant
+        interfaces = [graphene.relay.Node]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    def resolve_product_variant(root: product_models.ProductVariant, info):
+        visible_products = product_models.Product.objects.visible_to_user(
+            info.context.user
+        ).values_list("pk", flat=True)
+        return product_models.ProductVariant.objects.filter(
+            product__id__in=visible_products, pk=root.id
+        ).first()
 
 
 class ProductTranslation(BaseTranslationType):
     class Meta:
         model = product_models.ProductTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = [
-            "description",
-            "description_json",
-            "id",
-            "name",
-            "seo_title",
-            "seo_description",
-        ]
+        only_fields = EXTENDED_TRANSLATABLE_FIELDS
+
+
+class ProductTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        ProductTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description=("Returns translated Product fields for the given language code."),
+        resolver=resolve_translation,
+    )
+    product = graphene.Field(
+        "saleor.graphql.product.types.products.Product",
+        description="Represents an individual item for sale in the storefront.",
+    )
+
+    class Meta:
+        model = product_models.Product
+        interfaces = [graphene.relay.Node]
+        only_fields = EXTENDED_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    def resolve_product(root: product_models.Product, info):
+        return (
+            product_models.Product.objects.visible_to_user(info.context.user)
+            .filter(pk=root.id)
+            .first()
+        )
 
 
 class CollectionTranslation(BaseTranslationType):
     class Meta:
         model = product_models.CollectionTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = [
-            "description",
-            "description_json",
-            "id",
-            "name",
-            "seo_title",
-            "seo_description",
-        ]
+        only_fields = EXTENDED_TRANSLATABLE_FIELDS
+
+
+class CollectionTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        CollectionTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description=(
+            "Returns translated Collection fields for the given language code."
+        ),
+        resolver=resolve_translation,
+    )
+    collection = graphene.Field(
+        "saleor.graphql.product.types.products.Collection",
+        description="Represents a collection of products.",
+    )
+
+    class Meta:
+        model = product_models.Collection
+        interfaces = [graphene.relay.Node]
+        only_fields = EXTENDED_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    def resolve_collection(root: product_models.Collection, info):
+        return (
+            product_models.Collection.objects.visible_to_user(info.context.user)
+            .filter(pk=root.id)
+            .first()
+        )
 
 
 class CategoryTranslation(BaseTranslationType):
     class Meta:
         model = product_models.CategoryTranslation
         interfaces = [graphene.relay.Node]
+        only_fields = EXTENDED_TRANSLATABLE_FIELDS
+
+
+class CategoryTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        CategoryTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description=("Returns translated Category fields for the given language code."),
+        resolver=resolve_translation,
+    )
+    category = graphene.Field(
+        "saleor.graphql.product.types.products.Category",
+        description="Represents a single category of products.",
+    )
+
+    class Meta:
+        model = product_models.Category
+        interfaces = [graphene.relay.Node]
+        only_fields = EXTENDED_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    def resolve_category(root: product_models.Category, _info):
+        return root
+
+
+class PageTranslation(BaseTranslationType):
+    class Meta:
+        model = page_models.PageTranslation
+        interfaces = [graphene.relay.Node]
         only_fields = [
-            "description",
-            "description_json",
+            "content",
+            "content_json",
             "id",
-            "name",
-            "seo_title",
             "seo_description",
+            "seo_title",
+            "title",
         ]
 
 
-class PageTranslation(BaseTranslationType):
+class PageTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        PageTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description="Returns translated Page fields for the given language code.",
+        resolver=resolve_translation,
+    )
+    page = graphene.Field(
+        "saleor.graphql.page.types.Page",
+        description=(
+            "A static page that can be manually added by a shop operator ",
+            "through the dashboard.",
+        ),
+    )
+
     class Meta:
-        model = page_models.PageTranslation
+        model = page_models.Page
         interfaces = [graphene.relay.Node]
         only_fields = [
             "content",
@@ -112,19 +314,89 @@ class Meta:
             "title",
         ]
 
+    @staticmethod
+    def resolve_page(root: page_models.Page, info):
+        return (
+            page_models.Page.objects.visible_to_user(info.context.user)
+            .filter(pk=root.id)
+            .first()
+        )
+
 
 class VoucherTranslation(BaseTranslationType):
     class Meta:
         model = discount_models.VoucherTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = ["id", "name"]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+
+class VoucherTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        VoucherTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description="Returns translated Voucher fields for the given language code.",
+        resolver=resolve_translation,
+    )
+    voucher = graphene.Field(
+        "saleor.graphql.discount.types.Voucher",
+        # TODO consider move description to variable. Issue #4957
+        description=(
+            "Vouchers allow giving discounts to particular customers on categories, "
+            "collections or specific products. They can be used during checkout by "
+            "providing valid voucher codes."
+        ),
+    )
+
+    class Meta:
+        model = discount_models.Voucher
+        interfaces = [graphene.relay.Node]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    @permission_required("discount.manage_discounts")
+    def resolve_voucher(root: discount_models.Voucher, _info):
+        return root
 
 
 class SaleTranslation(BaseTranslationType):
     class Meta:
         model = discount_models.SaleTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = ["id", "name"]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+
+class SaleTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        SaleTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description="Returns translated sale fields for the given language code.",
+        resolver=resolve_translation,
+    )
+    sale = graphene.Field(
+        "saleor.graphql.discount.types.Sale",
+        description=(
+            "Sales allow creating discounts for categories, collections "
+            "or products and are visible to all the customers."
+        ),
+    )
+
+    class Meta:
+        model = discount_models.Sale
+        interfaces = [graphene.relay.Node]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    @permission_required("discount.manage_discounts")
+    def resolve_sale(root: discount_models.Sale, _info):
+        return root
 
 
 class ShopTranslation(BaseTranslationType):
@@ -138,11 +410,74 @@ class MenuItemTranslation(BaseTranslationType):
     class Meta:
         model = menu_models.MenuItemTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = ["id", "name"]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+
+class MenuItemTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        MenuItemTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description=(
+            "Returns translated Menu item fields for the given language code."
+        ),
+        resolver=resolve_translation,
+    )
+    menu_item = graphene.Field(
+        "saleor.graphql.menu.types.MenuItem",
+        description=(
+            "Represents a single item of the related menu. Can store categories, "
+            "collection or pages."
+        ),
+    )
+
+    class Meta:
+        model = menu_models.MenuItem
+        interfaces = [graphene.relay.Node]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    def resolve_menu_item(root: menu_models.MenuItem, _info):
+        return root
 
 
 class ShippingMethodTranslation(BaseTranslationType):
     class Meta:
         model = shipping_models.ShippingMethodTranslation
         interfaces = [graphene.relay.Node]
-        only_fields = ["id", "name"]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+
+class ShippingMethodTranslatableContent(CountableDjangoObjectType):
+    translation = graphene.Field(
+        ShippingMethodTranslation,
+        language_code=graphene.Argument(
+            LanguageCodeEnum,
+            description="A language code to return the translation for.",
+            required=True,
+        ),
+        description=(
+            "Returns translated shipping method fields for the given language code."
+        ),
+        resolver=resolve_translation,
+    )
+    shipping_method = graphene.Field(
+        "saleor.graphql.shipping.types.ShippingMethod",
+        description=(
+            "Shipping method are the methods you'll use to get customer's orders "
+            " to them. They are directly exposed to the customers."
+        ),
+    )
+
+    class Meta:
+        model = shipping_models.ShippingMethod
+        interfaces = [graphene.relay.Node]
+        only_fields = BASIC_TRANSLATABLE_FIELDS
+
+    @staticmethod
+    @permission_required("shipping.manage_shipping")
+    def resolve_shipping_method(root: shipping_models.ShippingMethod, _info):
+        return root
diff --git a/tests/api/test_translations.py b/tests/api/test_translations.py
index ab43659f375..373b6246c10 100644
--- a/tests/api/test_translations.py
+++ b/tests/api/test_translations.py
@@ -1,8 +1,10 @@
 import graphene
 import pytest
+from django.contrib.auth.models import Permission
 
 from saleor.graphql.translations.schema import TranslatableKinds
-from tests.api.utils import get_graphql_content
+from saleor.graphql.translations.types import LanguageCodeEnum
+from tests.api.utils import assert_no_permission, get_graphql_content
 
 
 def test_product_translation(user_api_client, product):
@@ -1361,3 +1363,633 @@ def test_translations_query_inline_fragment(user_api_client, product):
 
     assert data["node"]["name"] == "Test product"
     assert data["node"]["translation"]["name"] == "Produkt testowy"
+
+
+QUERY_TRANSLATION_PRODUCT = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on ProductTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                product{
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+@pytest.mark.parametrize(
+    "is_published, perm_codenames, return_product",
+    [
+        (True, ["manage_translations"], True),
+        (False, ["manage_translations"], False),
+        (False, ["manage_translations", "manage_products"], True),
+    ],
+)
+def test_translation_query_product(
+    staff_api_client,
+    product,
+    product_translation_fr,
+    is_published,
+    perm_codenames,
+    return_product,
+):
+    product.is_published = is_published
+    product.save()
+
+    product_id = graphene.Node.to_global_id("Product", product.id)
+    perms = list(Permission.objects.filter(codename__in=perm_codenames))
+
+    variables = {
+        "id": product_id,
+        "kind": TranslatableKinds.PRODUCT.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_PRODUCT, variables, permissions=perms
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["translation"]
+    assert data["name"] == product.name
+    assert data["translation"]["name"] == product_translation_fr.name
+    if return_product:
+        assert data["product"]["name"] == product.name
+    else:
+        assert not data["product"]
+
+
+QUERY_TRANSLATION_COLLECTION = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on CollectionTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                collection{
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+@pytest.mark.parametrize(
+    "is_published, perm_codenames, return_collection",
+    [
+        (True, ["manage_translations"], True),
+        (False, ["manage_translations"], False),
+        (False, ["manage_translations", "manage_products"], True),
+    ],
+)
+def test_translation_query_collection(
+    staff_api_client,
+    collection,
+    collection_translation_fr,
+    is_published,
+    perm_codenames,
+    return_collection,
+):
+    collection.is_published = is_published
+    collection.save()
+
+    collection_id = graphene.Node.to_global_id("Collection", collection.id)
+    perms = list(Permission.objects.filter(codename__in=perm_codenames))
+
+    variables = {
+        "id": collection_id,
+        "kind": TranslatableKinds.COLLECTION.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_COLLECTION, variables, permissions=perms
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["translation"]
+    assert data["name"] == collection.name
+    assert data["translation"]["name"] == collection_translation_fr.name
+    if return_collection:
+        assert data["collection"]["name"] == collection.name
+    else:
+        assert not data["collection"]
+
+
+QUERY_TRANSLATION_CATEGORY = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on CategoryTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                category {
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+def test_translation_query_category(
+    staff_api_client, category, category_translation_fr, permission_manage_translations
+):
+    category_id = graphene.Node.to_global_id("Category", category.id)
+
+    variables = {
+        "id": category_id,
+        "kind": TranslatableKinds.CATEGORY.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_CATEGORY,
+        variables,
+        permissions=[permission_manage_translations],
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["translation"]
+    assert data["name"] == category.name
+    assert data["translation"]["name"] == category_translation_fr.name
+    assert data["category"]["name"] == category.name
+
+
+QUERY_TRANSLATION_ATTRIBUTE = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on AttributeTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                attribute {
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+def test_translation_query_attribute(
+    staff_api_client, translated_attribute, permission_manage_translations
+):
+    attribute = translated_attribute.attribute
+    attribute_id = graphene.Node.to_global_id("Attribute", attribute.id)
+
+    variables = {
+        "id": attribute_id,
+        "kind": TranslatableKinds.ATTRIBUTE.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_ATTRIBUTE,
+        variables,
+        permissions=[permission_manage_translations],
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["translation"]
+    assert data["name"] == attribute.name
+    assert data["translation"]["name"] == translated_attribute.name
+    assert data["attribute"]["name"] == attribute.name
+
+
+QUERY_TRANSLATION_ATTRIBUTE_VALUE = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on AttributeValueTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                attributeValue {
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+def test_translation_query_attribute_value(
+    staff_api_client,
+    pink_attribute_value,
+    translated_attribute_value,
+    permission_manage_translations,
+):
+    attribute_value_id = graphene.Node.to_global_id(
+        "AttributeValue", pink_attribute_value.id
+    )
+
+    variables = {
+        "id": attribute_value_id,
+        "kind": TranslatableKinds.ATTRIBUTE_VALUE.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_ATTRIBUTE_VALUE,
+        variables,
+        permissions=[permission_manage_translations],
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["translation"]
+    assert data["name"] == pink_attribute_value.name
+    assert data["translation"]["name"] == translated_attribute_value.name
+    assert data["attributeValue"]["name"] == pink_attribute_value.name
+
+
+QUERY_TRANSLATION_VARIANT = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on ProductVariantTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                productVariant {
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+@pytest.mark.parametrize(
+    "is_published, perm_codenames, return_variant",
+    [
+        (True, ["manage_translations"], True),
+        (False, ["manage_translations"], False),
+        (False, ["manage_translations", "manage_products"], True),
+    ],
+)
+def test_translation_query_variant(
+    staff_api_client,
+    product,
+    variant,
+    variant_translation_fr,
+    is_published,
+    perm_codenames,
+    return_variant,
+):
+    product.is_published = is_published
+    product.save()
+
+    variant_id = graphene.Node.to_global_id("ProductVariant", variant.id)
+    perms = list(Permission.objects.filter(codename__in=perm_codenames))
+
+    variables = {
+        "id": variant_id,
+        "kind": TranslatableKinds.VARIANT.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_VARIANT, variables, permissions=perms
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["translation"]
+    assert data["name"] == variant.name
+    assert data["translation"]["name"] == variant_translation_fr.name
+    if return_variant:
+        assert data["productVariant"]["name"] == variant.name
+    else:
+        assert not data["productVariant"]
+
+
+QUERY_TRANSLATION_PAGE = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on PageTranslatableContent{
+                id
+                title
+                translation(languageCode: $languageCode){
+                    title
+                }
+                page {
+                    id
+                    title
+                }
+            }
+        }
+    }
+"""
+
+
+@pytest.mark.parametrize(
+    "is_published, perm_codenames, return_page",
+    [
+        (True, ["manage_translations"], True),
+        (False, ["manage_translations"], False),
+        (False, ["manage_translations", "manage_pages"], True),
+    ],
+)
+def test_translation_query_page(
+    staff_api_client,
+    page,
+    page_translation_fr,
+    is_published,
+    perm_codenames,
+    return_page,
+):
+    page.is_published = is_published
+    page.save()
+
+    page_id = graphene.Node.to_global_id("Page", page.id)
+    perms = list(Permission.objects.filter(codename__in=perm_codenames))
+
+    variables = {
+        "id": page_id,
+        "kind": TranslatableKinds.PAGE.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_PAGE, variables, permissions=perms
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["translation"]
+    assert data["title"] == page.title
+    assert data["translation"]["title"] == page_translation_fr.title
+    if return_page:
+        assert data["page"]["title"] == page.title
+    else:
+        assert not data["page"]
+
+
+QUERY_TRANSLATION_SHIPPING_METHOD = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on ShippingMethodTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                shippingMethod {
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+@pytest.mark.parametrize(
+    "perm_codenames, return_shipping_method",
+    [
+        (["manage_translations"], False),
+        (["manage_translations", "manage_shipping"], True),
+    ],
+)
+def test_translation_query_shipping_method(
+    staff_api_client,
+    shipping_method,
+    shipping_method_translation_fr,
+    perm_codenames,
+    return_shipping_method,
+):
+    shipping_method_id = graphene.Node.to_global_id(
+        "ShippingMethod", shipping_method.id
+    )
+    perms = list(Permission.objects.filter(codename__in=perm_codenames))
+
+    variables = {
+        "id": shipping_method_id,
+        "kind": TranslatableKinds.SHIPPING_METHOD.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_SHIPPING_METHOD, variables, permissions=perms
+    )
+    content = get_graphql_content(response, ignore_errors=True)
+    data = content["data"]["translation"]
+    assert data["name"] == shipping_method.name
+    assert data["translation"]["name"] == shipping_method_translation_fr.name
+    if return_shipping_method:
+        assert data["shippingMethod"]["name"] == shipping_method.name
+    else:
+        assert not data["shippingMethod"]
+
+
+QUERY_TRANSLATION_SALE = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on SaleTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                sale {
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+@pytest.mark.parametrize(
+    "perm_codenames, return_sale",
+    [
+        (["manage_translations"], False),
+        (["manage_translations", "manage_discounts"], True),
+    ],
+)
+def test_translation_query_sale(
+    staff_api_client, sale, sale_translation_fr, perm_codenames, return_sale
+):
+    sale_id = graphene.Node.to_global_id("Sale", sale.id)
+    perms = list(Permission.objects.filter(codename__in=perm_codenames))
+
+    variables = {
+        "id": sale_id,
+        "kind": TranslatableKinds.SALE.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_SALE, variables, permissions=perms
+    )
+    content = get_graphql_content(response, ignore_errors=True)
+    data = content["data"]["translation"]
+    assert data["name"] == sale.name
+    assert data["translation"]["name"] == sale_translation_fr.name
+    if return_sale:
+        assert data["sale"]["name"] == sale.name
+    else:
+        assert not data["sale"]
+
+
+QUERY_TRANSLATION_VOUCHER = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on VoucherTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                voucher {
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+@pytest.mark.parametrize(
+    "perm_codenames, return_voucher",
+    [
+        (["manage_translations"], False),
+        (["manage_translations", "manage_discounts"], True),
+    ],
+)
+def test_translation_query_voucher(
+    staff_api_client, voucher, voucher_translation_fr, perm_codenames, return_voucher
+):
+    voucher_id = graphene.Node.to_global_id("Voucher", voucher.id)
+    perms = list(Permission.objects.filter(codename__in=perm_codenames))
+
+    variables = {
+        "id": voucher_id,
+        "kind": TranslatableKinds.VOUCHER.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_VOUCHER, variables, permissions=perms
+    )
+    content = get_graphql_content(response, ignore_errors=True)
+    data = content["data"]["translation"]
+    assert data["name"] == voucher.name
+    assert data["translation"]["name"] == voucher_translation_fr.name
+    if return_voucher:
+        assert data["voucher"]["name"] == voucher.name
+    else:
+        assert not data["voucher"]
+
+
+QUERY_TRANSLATION_MENU_ITEM = """
+    query translation(
+        $kind: TranslatableKinds!, $id: ID!, $languageCode: LanguageCodeEnum!
+    ){
+        translation(kind: $kind, id: $id){
+            __typename
+            ...on MenuItemTranslatableContent{
+                id
+                name
+                translation(languageCode: $languageCode){
+                    name
+                }
+                menuItem {
+                    id
+                    name
+                }
+            }
+        }
+    }
+"""
+
+
+def test_translation_query_menu_item(
+    staff_api_client,
+    menu_item,
+    menu_item_translation_fr,
+    permission_manage_translations,
+):
+    menu_item_id = graphene.Node.to_global_id("MenuItem", menu_item.id)
+
+    variables = {
+        "id": menu_item_id,
+        "kind": TranslatableKinds.MENU_ITEM.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_MENU_ITEM,
+        variables,
+        permissions=[permission_manage_translations],
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["translation"]
+    assert data["name"] == menu_item.name
+    assert data["translation"]["name"] == menu_item_translation_fr.name
+    assert data["menuItem"]["name"] == menu_item.name
+
+
+def test_translation_query_incorrect_kind(
+    staff_api_client, menu_item, permission_manage_translations
+):
+    menu_item_id = graphene.Node.to_global_id("MenuItem", menu_item.id)
+
+    variables = {
+        "id": menu_item_id,
+        "kind": TranslatableKinds.PRODUCT.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(
+        QUERY_TRANSLATION_MENU_ITEM,
+        variables,
+        permissions=[permission_manage_translations],
+    )
+    content = get_graphql_content(response)
+    assert not content["data"]["translation"]
+
+
+def test_translation_query_no_permission(staff_api_client, menu_item):
+    menu_item_id = graphene.Node.to_global_id("MenuItem", menu_item.id)
+
+    variables = {
+        "id": menu_item_id,
+        "kind": TranslatableKinds.MENU_ITEM.name,
+        "languageCode": LanguageCodeEnum.FR.name,
+    }
+    response = staff_api_client.post_graphql(QUERY_TRANSLATION_MENU_ITEM, variables)
+    assert_no_permission(response)
diff --git a/tests/conftest.py b/tests/conftest.py
index 26bcd52a6ba..6a38867a012 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -22,16 +22,22 @@
 from saleor.checkout.utils import add_variant_to_checkout
 from saleor.core.payments import PaymentInterface
 from saleor.discount import DiscountInfo, DiscountValueType, VoucherType
-from saleor.discount.models import Sale, Voucher, VoucherCustomer, VoucherTranslation
+from saleor.discount.models import (
+    Sale,
+    SaleTranslation,
+    Voucher,
+    VoucherCustomer,
+    VoucherTranslation,
+)
 from saleor.giftcard.models import GiftCard
-from saleor.menu.models import Menu, MenuItem
+from saleor.menu.models import Menu, MenuItem, MenuItemTranslation
 from saleor.menu.utils import update_menu
 from saleor.order import OrderStatus
 from saleor.order.actions import fulfill_order_line
 from saleor.order.events import OrderEvents
 from saleor.order.models import FulfillmentStatus, Order, OrderEvent
 from saleor.order.utils import recalculate_order
-from saleor.page.models import Page
+from saleor.page.models import Page, PageTranslation
 from saleor.payment import ChargeStatus, TransactionKind
 from saleor.payment.models import Payment
 from saleor.product import AttributeInputType
@@ -39,8 +45,11 @@
     Attribute,
     AttributeTranslation,
     AttributeValue,
+    AttributeValueTranslation,
     Category,
+    CategoryTranslation,
     Collection,
+    CollectionTranslation,
     DigitalContent,
     DigitalContentUrl,
     Product,
@@ -48,9 +57,15 @@
     ProductTranslation,
     ProductType,
     ProductVariant,
+    ProductVariantTranslation,
 )
 from saleor.product.utils.attributes import associate_attribute_values_to_instance
-from saleor.shipping.models import ShippingMethod, ShippingMethodType, ShippingZone
+from saleor.shipping.models import (
+    ShippingMethod,
+    ShippingMethodTranslation,
+    ShippingMethodType,
+    ShippingZone,
+)
 from saleor.site import AuthenticationBackends
 from saleor.site.models import AuthorizationKey, SiteSettings
 from saleor.webhook import WebhookEventType
@@ -1266,7 +1281,16 @@ def translated_variant_fr(product):
 def translated_attribute(product):
     attribute = product.product_type.product_attributes.first()
     return AttributeTranslation.objects.create(
-        language_code="fr", attribute=attribute, name="Name tranlsated to french"
+        language_code="fr", attribute=attribute, name="French attribute name"
+    )
+
+
+@pytest.fixture
+def translated_attribute_value(pink_attribute_value):
+    return AttributeValueTranslation.objects.create(
+        language_code="fr",
+        attribute_value=pink_attribute_value,
+        name="French attribute value name",
     )
 
 
@@ -1287,6 +1311,66 @@ def product_translation_fr(product):
     )
 
 
+@pytest.fixture
+def variant_translation_fr(variant):
+    return ProductVariantTranslation.objects.create(
+        language_code="fr", product_variant=variant, name="French product variant name"
+    )
+
+
+@pytest.fixture
+def collection_translation_fr(collection):
+    return CollectionTranslation.objects.create(
+        language_code="fr",
+        collection=collection,
+        name="French collection name",
+        description="French description",
+    )
+
+
+@pytest.fixture
+def category_translation_fr(category):
+    return CategoryTranslation.objects.create(
+        language_code="fr",
+        category=category,
+        name="French category name",
+        description="French category description",
+    )
+
+
+@pytest.fixture
+def page_translation_fr(page):
+    return PageTranslation.objects.create(
+        language_code="fr",
+        page=page,
+        title="French page title",
+        content="French page content",
+    )
+
+
+@pytest.fixture
+def shipping_method_translation_fr(shipping_method):
+    return ShippingMethodTranslation.objects.create(
+        language_code="fr",
+        shipping_method=shipping_method,
+        name="French shipping method name",
+    )
+
+
+@pytest.fixture
+def sale_translation_fr(sale):
+    return SaleTranslation.objects.create(
+        language_code="fr", sale=sale, name="French sale name"
+    )
+
+
+@pytest.fixture
+def menu_item_translation_fr(menu_item):
+    return MenuItemTranslation.objects.create(
+        language_code="fr", menu_item=menu_item, name="French manu item name"
+    )
+
+
 @pytest.fixture
 def payment_dummy(db, order_with_lines):
     return Payment.objects.create(


Code review comments:
diff_hunk: @@ -37,33 +51,62 @@ class Meta:
         )
 
 
+# TODO Consider name of this class, we should replace to TranslatableItem after
+# `translations` query refactor. Issue #4957
+class DefaultTranslationItem(graphene.Union):
+    class Meta:
+        types = (
+            translation_types.ProductTranslatableContent,
+            translation_types.CollectionTranslatableContent,
+            translation_types.CategoryTranslatableContent,
+            translation_types.AttributeTranslatableContent,
+            translation_types.AttributeValueTranslatableContent,
+            translation_types.ProductVariantTranslatableContent,
+            translation_types.PageTranslatableContent,
+            translation_types.ShippingMethodTranslatableContent,
+            translation_types.SaleTranslatableContent,
+            translation_types.VoucherTranslatableContent,
+            translation_types.MenuItemTranslatableContent,
+        )
+
+
 class TranslatableItemConnection(CountableConnection):
     class Meta:
         node = TranslatableItem
 
 
 class TranslatableKinds(graphene.Enum):
     ATTRIBUTE = "Attribute"
-    ATTRIBUTE_VALUE = "Attribute value"
+    ATTRIBUTE_VALUE = "AttributeValue"
     CATEGORY = "Category"
     COLLECTION = "Collection"
-    MENU_ITEM = "Menu item"
+    MENU_ITEM = "MenuItem"
     PAGE = "Page"
     PRODUCT = "Product"
     SALE = "Sale"
-    SHIPPING_METHOD = "Shipping method"
-    VARIANT = "Variant"
+    SHIPPING_METHOD = "ShippingMethod"
+    VARIANT = "ProductVariant"
     VOUCHER = "Voucher"
 
 
 class TranslationQueries(graphene.ObjectType):
+    # TODO We nead to change output of this query to new types. Issue #4957
     translations = BaseConnectionField(
         TranslatableItemConnection,
         description="Returns a list of all translatable items of a given kind.",
         kind=graphene.Argument(
             TranslatableKinds, required=True, description="Kind of objects to retrieve."
         ),
     )
+    translation = graphene.Field(
+        DefaultTranslationItem,
+        id=graphene.Argument(
+            graphene.ID, description="ID of the objects to retrieve.", required=True
path: saleor/graphql/translations/schema.py
text: @user1:
```suggestion
            graphene.ID, description="ID of the object to retrieve.", required=True
```

diff_hunk: @@ -37,33 +51,62 @@ class Meta:
         )
 
 
+# TODO Consider name of this class, we should replace to TranslatableItem after
+# `translations` query refactor. Issue #4957
+class DefaultTranslationItem(graphene.Union):
+    class Meta:
+        types = (
+            translation_types.ProductTranslatableContent,
+            translation_types.CollectionTranslatableContent,
+            translation_types.CategoryTranslatableContent,
+            translation_types.AttributeTranslatableContent,
+            translation_types.AttributeValueTranslatableContent,
+            translation_types.ProductVariantTranslatableContent,
+            translation_types.PageTranslatableContent,
+            translation_types.ShippingMethodTranslatableContent,
+            translation_types.SaleTranslatableContent,
+            translation_types.VoucherTranslatableContent,
+            translation_types.MenuItemTranslatableContent,
+        )
+
+
 class TranslatableItemConnection(CountableConnection):
     class Meta:
         node = TranslatableItem
 
 
 class TranslatableKinds(graphene.Enum):
     ATTRIBUTE = "Attribute"
-    ATTRIBUTE_VALUE = "Attribute value"
+    ATTRIBUTE_VALUE = "AttributeValue"
     CATEGORY = "Category"
     COLLECTION = "Collection"
-    MENU_ITEM = "Menu item"
+    MENU_ITEM = "MenuItem"
     PAGE = "Page"
     PRODUCT = "Product"
     SALE = "Sale"
-    SHIPPING_METHOD = "Shipping method"
-    VARIANT = "Variant"
+    SHIPPING_METHOD = "ShippingMethod"
+    VARIANT = "ProductVariant"
     VOUCHER = "Voucher"
 
 
 class TranslationQueries(graphene.ObjectType):
+    # TODO We nead to change output of this query to new types. Issue #4957
     translations = BaseConnectionField(
         TranslatableItemConnection,
         description="Returns a list of all translatable items of a given kind.",
         kind=graphene.Argument(
             TranslatableKinds, required=True, description="Kind of objects to retrieve."
         ),
     )
+    translation = graphene.Field(
+        DefaultTranslationItem,
+        id=graphene.Argument(
+            graphene.ID, description="ID of the objects to retrieve.", required=True
+        ),
+        kind=graphene.Argument(
+            TranslatableKinds, required=True, description="Kind of objects to retrieve."
path: saleor/graphql/translations/schema.py
text: @user1:
```suggestion
            TranslatableKinds, required=True, description="Kind of the object to retrieve."
```