Repository: shopware/frontends (ts)

Issue:
[BUG] Error message for increasing product quantity above stock
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior


When we try to increase the quantity of a product above the stock level, we receive a message without information about the available stock
 
<img width="1456" alt="image" src="https://github.com/user-attachments/assets/62b6f7f5-91bc-4c99-a1d3-046f4c77938c">


### Expected Behavior

_No response_

### Steps To Reproduce

_No response_

### Environment

```markdown
- OS:
- Node:
- pnpm:
```


### Anything else?

_No response_

Pull request:
feat(vue-demo): message for increasing product quantity above stock
### Description

- Added `promotions-on-cart-price-zero-error` error translation
- Added `product-stock-reached-empty` error translation
- Fixed add to cart notifications
- Added `useCmsTranslate` composable for replacing placeholders
- Added `useCartErrorParamsResolver` composable for building error message parameters
- Added `getItem` method to `useCart` for getting single item from cart


closes #1167 #782

### Type of change

<!-- Comment out the type of change your PR is making -->

<!-- Bug fix (non-breaking change that fixes an issue) -->
<!-- New feature (non-breaking change which adds functionality) -->
<!-- Breaking change (fix or feature that would cause existing functionality to not work as expected) -->

### ToDo's

<!-- Add the todo's that need to be done before merge -->
<!-- Remember to run `pnpm run changeset` and describe your change (plus potential migration guide/important notes) to your pull request. -->

<!-- - [ ] Changeset file provided [read more](https://github.com/shopware/frontends/blob/main/CONTRIBUTION.md#changelog-preparation) -->
<!-- - [ ] Documentation added/updated -->
<!-- - [ ] Unit-Tests added/updated -->
<!-- - [ ] E2E-Tests added/updated -->
<!-- - [ ] Related Issue updated -->

### Screenshots (if applicable)

<!-- Please attach any relevant screenshots or images to help explain your changes. -->

### Additional context

<!-- Add any other context about the pull request here. -->


Patch:
diff --git a/.changeset/fresh-items-admire.md b/.changeset/fresh-items-admire.md
new file mode 100644
index 00000000..47e72fd6
--- /dev/null
+++ b/.changeset/fresh-items-admire.md
@@ -0,0 +1,5 @@
+---
+"@shopware-pwa/composables-next": minor
+---
+
+- Added `useCartErrorParamsResolver` composable for building error message parameters
diff --git a/.changeset/grumpy-moose-occur.md b/.changeset/grumpy-moose-occur.md
new file mode 100644
index 00000000..6eb8668a
--- /dev/null
+++ b/.changeset/grumpy-moose-occur.md
@@ -0,0 +1,5 @@
+---
+"@shopware-pwa/helpers-next": minor
+---
+
+Added `getCmsTranslate` helper to replace text placeholder with param value
diff --git a/.changeset/yellow-grapes-compete.md b/.changeset/yellow-grapes-compete.md
new file mode 100644
index 00000000..ad44dedc
--- /dev/null
+++ b/.changeset/yellow-grapes-compete.md
@@ -0,0 +1,7 @@
+---
+"vue-demo-store": minor
+---
+
+- Added `promotions-on-cart-price-zero-error` error translation
+- Added `product-stock-reached-empty` error translation
+- Fixed add to cart notificationsz
diff --git a/packages/cms-base/components/SwProductAddToCart.vue b/packages/cms-base/components/SwProductAddToCart.vue
index 8ee4ebf8..4c27d144 100644
--- a/packages/cms-base/components/SwProductAddToCart.vue
+++ b/packages/cms-base/components/SwProductAddToCart.vue
@@ -1,11 +1,19 @@
 <script setup lang="ts">
 import { useCmsTranslations } from "@shopware-pwa/composables-next";
-import { useAddToCart, useNotifications } from "#imports";
+import {
+  useAddToCart,
+  useNotifications,
+  useCartNotification,
+  useCartErrorParamsResolver,
+} from "#imports";
 import { toRefs } from "vue";
 import { defu } from "defu";
 import type { Schemas } from "#shopware";
+import { getCmsTranslate } from "@shopware-pwa/helpers-next";
 
-const { pushSuccess } = useNotifications();
+const { pushSuccess, pushError } = useNotifications();
+const { getErrorsCodes } = useCartNotification();
+const { resolveCartError } = useCartErrorParamsResolver();
 const props = defineProps<{
   product: Schemas["Product"];
 }>();
@@ -16,6 +24,9 @@ type Translations = {
     qty: string;
     addToCart: string;
   };
+  errors: {
+    [key: string]: string;
+  };
 };
 
 let translations: Translations = {
@@ -24,6 +35,10 @@ let translations: Translations = {
     qty: "Qty",
     addToCart: "Add to cart",
   },
+  errors: {
+    "product-stock-reached":
+      "The product {name} is only available {quantity} times",
+  },
 };
 
 translations = defu(useCmsTranslations(), translations) as Translations;
@@ -33,9 +48,16 @@ const { addToCart, quantity } = useAddToCart(product);
 
 const addToCartProxy = async () => {
   await addToCart();
-  pushSuccess(
-    `${props.product?.translated.name} ${translations.product.addedToCart}`,
-  );
+  const errors = getErrorsCodes();
+  errors?.forEach((element) => {
+    const { messageKey, params } = resolveCartError(element);
+    pushError(getCmsTranslate(translations.errors[messageKey], params));
+  });
+
+  if (!errors.length)
+    pushSuccess(
+      `${props.product?.translated.name} ${translations.product.addedToCart}`,
+    );
 };
 </script>
 
diff --git a/packages/cms-base/components/SwProductCard.vue b/packages/cms-base/components/SwProductCard.vue
index 50519a43..8a30db99 100644
--- a/packages/cms-base/components/SwProductCard.vue
+++ b/packages/cms-base/components/SwProductCard.vue
@@ -17,12 +17,17 @@ import {
   useNotifications,
   useProductWishlist,
   useUrlResolver,
+  useCartNotification,
+  useCartErrorParamsResolver,
 } from "#imports";
 import { useElementSize } from "@vueuse/core";
 import type { Schemas } from "#shopware";
 import { ApiClientError } from "@shopware/api-client";
+import { getCmsTranslate } from "@shopware-pwa/helpers-next";
 
 const { pushSuccess, pushError } = useNotifications();
+const { getErrorsCodes } = useCartNotification();
+const { resolveCartError } = useCartErrorParamsResolver();
 
 const props = withDefaults(
   defineProps<{
@@ -51,6 +56,9 @@ type Translations = {
       topseller: string;
     };
   };
+  errors: {
+    [key: string]: string;
+  };
 };
 
 let translations: Translations = {
@@ -66,6 +74,10 @@ let translations: Translations = {
       topseller: "Tip",
     },
   },
+  errors: {
+    "product-stock-reached":
+      "The product {name} is only available {quantity} times",
+  },
 };
 
 translations = defu(useCmsTranslations(), translations) as Translations;
@@ -113,9 +125,16 @@ const toggleWishlistProduct = async () => {
 
 const addToCartProxy = async () => {
   await addToCart();
-  pushSuccess(
-    `${props.product?.translated.name} ${translations.product.addedToCart}`,
-  );
+  const errors = getErrorsCodes();
+  errors?.forEach((element) => {
+    const { messageKey, params } = resolveCartError(element);
+    pushError(getCmsTranslate(translations.errors[messageKey], params));
+  });
+
+  if (!errors.length)
+    pushSuccess(
+      `${props.product?.translated.name} ${translations.product.addedToCart}`,
+    );
 };
 
 const fromPrice = getProductFromPrice(props.product);
diff --git a/packages/composables/src/index.ts b/packages/composables/src/index.ts
index 79ab1d4b..28919558 100644
--- a/packages/composables/src/index.ts
+++ b/packages/composables/src/index.ts
@@ -62,6 +62,8 @@ export * from "./useUrlResolver";
 export * from "./useUser";
 export * from "./useWishlist";
 export * from "./useB2bQuoteManagement";
+export * from "./useCartNotification";
+export * from "./useCartErrorParamsResolver";
 
 export function resolveCmsComponent(
   content: Schemas["CmsSection"] | Schemas["CmsBlock"] | Schemas["CmsSlot"],
diff --git a/packages/composables/src/useCartErrorParamsResolver.ts b/packages/composables/src/useCartErrorParamsResolver.ts
new file mode 100644
index 00000000..97653fac
--- /dev/null
+++ b/packages/composables/src/useCartErrorParamsResolver.ts
@@ -0,0 +1,66 @@
+import { useCart } from "#imports";
+
+export type UseCartErrorParamsResolver = {
+  resolveCartError: (errorObject: ErrorObjectParam) => {
+    params: { [key: string]: string | number | null } | null;
+    messageKey: string;
+  };
+};
+
+export type ErrorObjectParam = {
+  messageKey: string;
+  key: string;
+  message?: string;
+  code?: number;
+  level?: number;
+};
+
+export function useCartErrorParamsResolver(): UseCartErrorParamsResolver {
+  const resolveCartError = (errorObject: ErrorObjectParam) => {
+    let params = null;
+    let messageKey = errorObject.messageKey;
+
+    const { cartItems } = useCart();
+
+    const extractProductId = (message: string, error: string) => {
+      return message.replace(error, "");
+    };
+
+    const buildProductStockReached = () => {
+      const productId = extractProductId(
+        errorObject.key,
+        "product-stock-reached",
+      );
+      const product = getItem(productId);
+
+      return {
+        name: product?.label || "",
+        quantity: product?.quantityInformation?.maxPurchase || null,
+      };
+    };
+
+    const getItem = (id: string) => {
+      return cartItems.value.find((item) => item.id === id);
+    };
+
+    switch (errorObject.messageKey) {
+      case "product-stock-reached":
+        params = buildProductStockReached();
+        if (!params.name || !params.quantity) {
+          messageKey = "product-stock-reached-empty";
+          params = null;
+        }
+        break;
+      default:
+        params = { ...errorObject };
+    }
+
+    return {
+      params,
+      messageKey,
+    };
+  };
+  return {
+    resolveCartError,
+  };
+}
diff --git a/templates/vue-demo-store/composables/useCartNotification.ts b/packages/composables/src/useCartNotification.ts
similarity index 92%
rename from templates/vue-demo-store/composables/useCartNotification.ts
rename to packages/composables/src/useCartNotification.ts
index 74df465a..3a5e2fe4 100644
--- a/templates/vue-demo-store/composables/useCartNotification.ts
+++ b/packages/composables/src/useCartNotification.ts
@@ -1,4 +1,5 @@
 import type { Schemas } from "#shopware";
+import { useCart, useNotifications } from "#imports";
 
 const successCodes = ["promotion-discount-added"];
 
@@ -25,13 +26,13 @@ export function useCartNotification(): useCartNotificationReturn {
     const errors: Schemas["Cart"]["errors"] = consumeCartErrors();
     if (!errors || Array.isArray(errors)) return;
 
-    Object.keys(errors).forEach((element) => {
+    for (const element of Object.keys(errors)) {
       if (successCodes.includes(errors[element].messageKey)) {
         pushSuccess(errors[element].message);
       } else {
         pushError(errors[element].message);
       }
-    });
+    }
   };
 
   /**
diff --git a/packages/helpers/src/cms/getCmsTranslate.ts b/packages/helpers/src/cms/getCmsTranslate.ts
new file mode 100644
index 00000000..76c64f00
--- /dev/null
+++ b/packages/helpers/src/cms/getCmsTranslate.ts
@@ -0,0 +1,25 @@
+/**
+ * Replace text placeholder with param value
+ *
+ * @param {string} key
+ * @param {} params
+ * @returns {string}
+ */
+export function getCmsTranslate(
+  key: string,
+  params?: { [key: string]: string | number | null } | null,
+) {
+  if (!params) return key;
+
+  let translatedText = key;
+  for (const param in params) {
+    const placeholder = `{${param}}`;
+    const paramValue = params[param];
+    if (paramValue)
+      translatedText = translatedText.replace(
+        placeholder,
+        paramValue?.toString(),
+      );
+  }
+  return translatedText;
+}
diff --git a/packages/helpers/src/cms/index.ts b/packages/helpers/src/cms/index.ts
index e4a7436a..f70ba63d 100644
--- a/packages/helpers/src/cms/index.ts
+++ b/packages/helpers/src/cms/index.ts
@@ -25,6 +25,7 @@ export * from "./getBackgroundImageUrl";
 export * from "./buildUrlPrefix";
 export * from "./layoutClasses";
 export * from "./isMaintenanceMode";
+export * from "./getCmsTranslate";
 
 /**
  * Returns the main page object depending of the type of the CMS page.
diff --git a/templates/vue-demo-store/components/checkout/cart/ProductItem.vue b/templates/vue-demo-store/components/checkout/cart/ProductItem.vue
index 06d05652..936864ed 100644
--- a/templates/vue-demo-store/components/checkout/cart/ProductItem.vue
+++ b/templates/vue-demo-store/components/checkout/cart/ProductItem.vue
@@ -6,6 +6,7 @@ import { ApiClientError } from "@shopware/api-client";
 const miniCartModal = useMiniCartModal();
 const localePath = useLocalePath();
 const { formatLink } = useInternationalization(localePath);
+const { resolveCartError } = useCartErrorParamsResolver();
 
 const props = withDefaults(
   defineProps<{
@@ -58,11 +59,9 @@ const updateQuantity = async (quantityInput: number | undefined) => {
     }
   }
 
-  // Make sure that qty is the same as it is in the response
-  quantity.value = itemQuantity.value;
-
   getErrorsCodes()?.forEach((element) => {
-    pushError(t(`errors.${element.messageKey}`, { ...element }));
+    const { messageKey, params } = resolveCartError(element);
+    pushError(t(`errors.${messageKey}`, params as Record<string, unknown>));
   });
 
   isLoading.value = false;
diff --git a/templates/vue-demo-store/components/product/ProductCard.vue b/templates/vue-demo-store/components/product/ProductCard.vue
index 645fcf8b..eb6dead7 100644
--- a/templates/vue-demo-store/components/product/ProductCard.vue
+++ b/templates/vue-demo-store/components/product/ProductCard.vue
@@ -14,6 +14,7 @@ const { t } = useI18n();
 const { getErrorsCodes } = useCartNotification();
 const localePath = useLocalePath();
 const { formatLink } = useInternationalization(localePath);
+const { resolveCartError } = useCartErrorParamsResolver();
 
 const props = withDefaults(
   defineProps<{
@@ -60,13 +61,17 @@ const toggleWishlistProduct = async () => {
 
 const addToCartProxy = async () => {
   await addToCart();
+  const errors = getErrorsCodes();
 
-  getErrorsCodes()?.forEach((element) => {
-    pushError(t(`errors.${element.messageKey}`, { ...element }));
+  errors?.forEach((element) => {
+    const { messageKey, params } = resolveCartError(element);
+    pushError(t(`errors.${messageKey}`, params as Record<string, unknown>));
   });
-  pushSuccess(
-    t(`cart.messages.addedToCart`, { p: props.product?.translated.name }),
-  );
+
+  if (!errors.length)
+    pushSuccess(
+      t(`cart.messages.addedToCart`, { p: props.product?.translated.name }),
+    );
 };
 
 const fromPrice = getProductFromPrice(props.product);
diff --git a/templates/vue-demo-store/i18n/de-DE/errors.json b/templates/vue-demo-store/i18n/de-DE/errors.json
index 3be16ea4..12d7fbc8 100644
--- a/templates/vue-demo-store/i18n/de-DE/errors.json
+++ b/templates/vue-demo-store/i18n/de-DE/errors.json
@@ -1,6 +1,8 @@
 {
   "errors": {
+    "promotions-on-cart-price-zero-error": "Promotionen sind für den Warenkorb ausgeschlossen, da der Preis des Warenkorbs null ist",
     "product-stock-reached": "Das Produkt {name} ist nur noch {quantity} mal verfügbar",
+    "product-stock-reached-empty": "Diese Menge kann nicht hinzugefügt werden",
     "product-out-of-stock": "Das Produkt {name} ist nicht mehr verfügbar",
     "purchase-steps-quantity": "Das Produkt {name} ist nicht in der gewünschten Menge verfügbar. Die Anzahl wurde auf {quantity} geändert.",
     "min-order-quantity": "Die Anzahl an Produkten {name} hat die Mindestbestellmenge nicht erreicht. Die Anzahl wurde automatisch auf {quantity} erhöht.",
diff --git a/templates/vue-demo-store/i18n/en-GB/errors.json b/templates/vue-demo-store/i18n/en-GB/errors.json
index c6ceea36..d4940e39 100644
--- a/templates/vue-demo-store/i18n/en-GB/errors.json
+++ b/templates/vue-demo-store/i18n/en-GB/errors.json
@@ -1,6 +1,8 @@
 {
   "errors": {
+    "promotions-on-cart-price-zero-error": "Promotions are excluded for cart because the price of the cart is zero",
     "product-stock-reached": "The product {name} is only available {quantity} times",
+    "product-stock-reached-empty": "Cannot add this quantity",
     "product-out-of-stock": "The product {name} is not available any more",
     "purchase-steps-quantity": "The product {name} is not available in this quantity. The quantity was changed to {quantity}",
     "min-order-quantity": "The quantity of product {name} did not meet the minimum order quantity threshold. The quantity has automatically been increased to {quantity}.",
diff --git a/templates/vue-demo-store/i18n/pl-PL/errors.json b/templates/vue-demo-store/i18n/pl-PL/errors.json
index b33bd3f3..44ec4179 100644
--- a/templates/vue-demo-store/i18n/pl-PL/errors.json
+++ b/templates/vue-demo-store/i18n/pl-PL/errors.json
@@ -1,6 +1,8 @@
 {
   "errors": {
+    "promotions-on-cart-price-zero-error": "Promocje są wyłączone dla koszyka, ponieważ wartość koszyka wynosi zero",
     "product-stock-reached": "Produkt {name} jest dostępny tylko {quantity} razy",
+    "product-stock-reached-empty": "Nie można dodać takiej ilości",
     "product-out-of-stock": "Produkt {name} nie jest już dostępny",
     "purchase-steps-quantity": "Produkt {name} nie jest dostępny w żądanej ilości. Ilość została zmieniona na {quantity}.",
     "min-order-quantity": "Liczba produktów {name} nie osiągnęła minimalnej ilości zamówienia. Ilość została automatycznie zwiększona do {quantity}.",


Test patch:
diff --git a/packages/composables/src/useCartErrorParamsResolver.test.ts b/packages/composables/src/useCartErrorParamsResolver.test.ts
new file mode 100644
index 00000000..27344c98
--- /dev/null
+++ b/packages/composables/src/useCartErrorParamsResolver.test.ts
@@ -0,0 +1,82 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { useCartErrorParamsResolver } from "./useCartErrorParamsResolver";
+import { useCart } from "./useCart";
+import { ref } from "vue";
+
+vi.mock("./useCart.ts");
+
+describe("useCartErrorParamsResolver", () => {
+  beforeEach(() => {
+    vi.resetAllMocks();
+  });
+
+  it("should return resolved product-stock-reached error", () => {
+    vi.mocked(useCart).mockReturnValue({
+      cartItems: ref([
+        {
+          id: "c29ad46d64474ef0b72845832f0d879b",
+          label: "Greta Glass drink dispenser 5 l with tap and wooden stand",
+          quantityInformation: {
+            maxPurchase: 5,
+          },
+        },
+      ]),
+    } as unknown as ReturnType<typeof useCart>);
+    const { resolveCartError } = useCartErrorParamsResolver();
+    const error = resolveCartError({
+      message:
+        "The product Greta Glass drink dispenser 5 l with tap and wooden stand is only available 5 times",
+      code: 0,
+      key: "product-stock-reachedc29ad46d64474ef0b72845832f0d879b",
+      level: 10,
+      messageKey: "product-stock-reached",
+    });
+
+    expect(error).toEqual({
+      params: {
+        name: "Greta Glass drink dispenser 5 l with tap and wooden stand",
+        quantity: 5,
+      },
+      messageKey: "product-stock-reached",
+    });
+  });
+
+  it("should return resolved product-stock-reached-empty error if there is no product data", () => {
+    vi.mocked(useCart).mockReturnValue({
+      cartItems: ref([]),
+    } as unknown as ReturnType<typeof useCart>);
+    const { resolveCartError } = useCartErrorParamsResolver();
+    const error = resolveCartError({
+      message:
+        "The product Greta Glass drink dispenser 5 l with tap and wooden stand is only available 5 times",
+      code: 0,
+      key: "product-stock-reachedc29ad46d64474ef0b72845832f0d879b",
+      level: 10,
+      messageKey: "product-stock-reached",
+    });
+
+    expect(error).toEqual({
+      params: null,
+      messageKey: "product-stock-reached-empty",
+    });
+  });
+
+  it("should return passed params if error is not supported", () => {
+    vi.mocked(useCart).mockReturnValue({
+      cartItems: ref([]),
+    } as unknown as ReturnType<typeof useCart>);
+    const { resolveCartError } = useCartErrorParamsResolver();
+    const error = resolveCartError({
+      key: "custom-errorc29ad46d64474ef0b72845832f0d879b",
+      messageKey: "custom-error",
+    });
+
+    expect(error).toEqual({
+      params: {
+        key: "custom-errorc29ad46d64474ef0b72845832f0d879b",
+        messageKey: "custom-error",
+      },
+      messageKey: "custom-error",
+    });
+  });
+});
diff --git a/packages/composables/src/useCartNotification.test.ts b/packages/composables/src/useCartNotification.test.ts
new file mode 100644
index 00000000..013758e1
--- /dev/null
+++ b/packages/composables/src/useCartNotification.test.ts
@@ -0,0 +1,130 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { useCartNotification } from "./useCartNotification";
+import { useSetup } from "./_test";
+import { useCart } from "./useCart";
+
+const pushErrorSpy = vi.fn();
+const pushSuccessSpy = vi.fn();
+
+vi.mock("./useNotifications.ts", async () => {
+  return {
+    useNotifications: () => {
+      return {
+        pushError: pushErrorSpy,
+        pushSuccess: pushSuccessSpy,
+      };
+    },
+  };
+});
+
+vi.mock("./useCart.ts");
+
+describe("useCartNotification", () => {
+  beforeEach(() => {
+    vi.resetAllMocks();
+  });
+
+  it("should display success notification", () => {
+    vi.mocked(useCart).mockReturnValue({
+      consumeCartErrors: vi.fn(() => {
+        return JSON.parse(
+          JSON.stringify({
+            "promotion-discount-added-1326f60448d84aed964d77ca1c2dde90": {
+              message: "Discount Your lucky day! has been added",
+              code: 0,
+              key: "promotion-discount-added-1326f60448d84aed964d77ca1c2dde90",
+              level: 0,
+              messageKey: "promotion-discount-added",
+            },
+          }),
+        );
+      }),
+    } as unknown as ReturnType<typeof useCart>);
+
+    const { vm } = useSetup(() => useCartNotification());
+    vm.codeErrorsNotification();
+    expect(pushSuccessSpy).toBeCalledWith(
+      "Discount Your lucky day! has been added",
+    );
+  });
+
+  it("should return empty array if errors are not present", () => {
+    vi.mocked(useCart).mockReturnValue({
+      consumeCartErrors: vi.fn(() => {
+        return null;
+      }),
+    } as unknown as ReturnType<typeof useCart>);
+
+    const { vm } = useSetup(() => useCartNotification());
+    vm.codeErrorsNotification();
+    expect(pushSuccessSpy).not.toBeCalledWith();
+    expect(pushErrorSpy).not.toBeCalledWith();
+  });
+
+  it("should display error notification", () => {
+    vi.mocked(useCart).mockReturnValue({
+      consumeCartErrors: vi.fn(() => {
+        return JSON.parse(
+          JSON.stringify({
+            "product-stock-reachedc29ad46d64474ef0b72845832f0d879b": {
+              message:
+                "The product Greta Glass drink dispenser 5 l with tap and wooden stand is only available 5 times",
+              code: 0,
+              key: "product-stock-reachedc29ad46d64474ef0b72845832f0d879b",
+              level: 10,
+              messageKey: "product-stock-reached",
+            },
+          }),
+        );
+      }),
+    } as unknown as ReturnType<typeof useCart>);
+
+    const { vm } = useSetup(() => useCartNotification());
+    vm.codeErrorsNotification();
+    expect(pushErrorSpy).toBeCalledWith(
+      "The product Greta Glass drink dispenser 5 l with tap and wooden stand is only available 5 times",
+    );
+  });
+
+  it("should return errors codes", () => {
+    vi.mocked(useCart).mockReturnValue({
+      consumeCartErrors: vi.fn(() => {
+        return JSON.parse(
+          JSON.stringify({
+            "product-stock-reachedc29ad46d64474ef0b72845832f0d879b": {
+              message:
+                "The product Greta Glass drink dispenser 5 l with tap and wooden stand is only available 5 times",
+              code: 0,
+              key: "product-stock-reachedc29ad46d64474ef0b72845832f0d879b",
+              level: 10,
+              messageKey: "product-stock-reached",
+            },
+          }),
+        );
+      }),
+    } as unknown as ReturnType<typeof useCart>);
+    const { vm } = useSetup(() => useCartNotification());
+
+    expect(vm.getErrorsCodes()).toEqual([
+      {
+        code: 0,
+        key: "product-stock-reachedc29ad46d64474ef0b72845832f0d879b",
+        level: 10,
+        message:
+          "The product Greta Glass drink dispenser 5 l with tap and wooden stand is only available 5 times",
+        messageKey: "product-stock-reached",
+      },
+    ]);
+  });
+
+  it("should return empty array if errors are not present", () => {
+    vi.mocked(useCart).mockReturnValue({
+      consumeCartErrors: vi.fn(() => {
+        return null;
+      }),
+    } as unknown as ReturnType<typeof useCart>);
+
+    const { vm } = useSetup(() => useCartNotification());
+    expect(vm.getErrorsCodes()).toEqual([]);
+  });
+});
diff --git a/packages/helpers/src/cms/getCmsTranslate.test.ts b/packages/helpers/src/cms/getCmsTranslate.test.ts
new file mode 100644
index 00000000..2ca639a6
--- /dev/null
+++ b/packages/helpers/src/cms/getCmsTranslate.test.ts
@@ -0,0 +1,24 @@
+import { getCmsTranslate } from "./getCmsTranslate";
+import { describe, expect, it } from "vitest";
+
+describe("useCmsTranslate", () => {
+  it("should replace placeholder with param value", async () => {
+    expect(getCmsTranslate("Hello, {name}!", { name: "world" })).toBe(
+      "Hello, world!",
+    );
+  });
+
+  it("should return placeholder if param is missing", async () => {
+    expect(getCmsTranslate("Hello, {name}!", {})).toBe("Hello, {name}!");
+  });
+
+  it("should return text without placeholder (params added)", async () => {
+    expect(getCmsTranslate("Hello, world!", { name: "world" })).toBe(
+      "Hello, world!",
+    );
+  });
+
+  it("should return text without placeholder (params missing)", async () => {
+    expect(getCmsTranslate("Hello, world!")).toBe("Hello, world!");
+  });
+});
diff --git a/packages/helpers/src/index.test.ts b/packages/helpers/src/index.test.ts
index 5775bc7d..0b2ac63c 100644
--- a/packages/helpers/src/index.test.ts
+++ b/packages/helpers/src/index.test.ts
@@ -16,6 +16,7 @@ describe("helpers - test global API", () => {
         "getCategoryUrl": [Function],
         "getCmsEntityObject": [Function],
         "getCmsLayoutConfiguration": [Function],
+        "getCmsTranslate": [Function],
         "getFormattedPrice": [Function],
         "getLanguageName": [Function],
         "getListingFilters": [Function],