@vtex/faststore-plugin-buyer-portal 2.0.13 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/cms/b2b/components/.gitkeep +0 -0
  3. package/cms/b2b/pages/cms_content_type__i18n.jsonc +36 -0
  4. package/cypress/integration/localization.test.ts +482 -0
  5. package/cypress/support/commands.ts +8 -0
  6. package/docs/i18n/i18n-messages-format.md +391 -0
  7. package/docs/i18n/locales.json +701 -0
  8. package/docs/i18n/migration-inventory.md +959 -0
  9. package/package.json +3 -3
  10. package/specs/custom-localization-system.md +711 -0
  11. package/specs/multi-locale-e2e-validation.md +277 -0
  12. package/src/__mocks__/discovery.config.ts +12 -0
  13. package/src/features/accounting-fields/components/AccountingConfirmDelete/AccountingConfirmDelete.tsx +14 -4
  14. package/src/features/accounting-fields/components/AccountingFieldConfirmEdit/AccountingFieldConfirmEdit.tsx +11 -6
  15. package/src/features/accounting-fields/components/AccountingFieldDropdown/AccountingFieldDropdown.tsx +5 -3
  16. package/src/features/accounting-fields/components/AccountingFieldForm/AccountingFieldForm.tsx +33 -23
  17. package/src/features/accounting-fields/components/AccountingValueConfirmEdit/AccountingValueConfirmEdit.tsx +10 -6
  18. package/src/features/accounting-fields/components/AccountingValueDropdown/AccountingValueDropdown.tsx +19 -9
  19. package/src/features/accounting-fields/components/AccountingValueForm/AccountingValueForm.tsx +21 -10
  20. package/src/features/accounting-fields/components/CreateAccountingFieldDrawer/CreateAccountingFieldDrawer.tsx +11 -8
  21. package/src/features/accounting-fields/components/CreateAccountingValueDrawer/CreateAccountingValueDrawer.tsx +18 -7
  22. package/src/features/accounting-fields/components/DeleteAccountingFieldDrawer/DeleteAccountingFieldDrawer.tsx +14 -12
  23. package/src/features/accounting-fields/components/DeleteAccountingValueDrawer/DeleteAccountingValueDrawer.tsx +20 -10
  24. package/src/features/accounting-fields/components/EditAccountingFieldDrawer/EditAccountingFieldDrawer.tsx +5 -3
  25. package/src/features/accounting-fields/components/EditAccountingValueDrawer/EditAccountingValueDrawer.tsx +13 -6
  26. package/src/features/accounting-fields/components/RemoveValueFromUnitDrawer/RemoveValueFromUnitDrawer.tsx +19 -8
  27. package/src/features/accounting-fields/layouts/AccountingFieldLayout/AccountingFieldLayout.tsx +11 -9
  28. package/src/features/addresses/components/AddRecipientsDrawer/AddRecipientsDrawer.tsx +13 -7
  29. package/src/features/addresses/components/AddressDropdownMenu/AddressDropdownMenu.tsx +22 -20
  30. package/src/features/addresses/components/AddressForm/AddressForm.tsx +33 -22
  31. package/src/features/addresses/components/AddressLine/AddressLine.tsx +29 -25
  32. package/src/features/addresses/components/AddressLocationsList/AddressLocationsList.tsx +15 -6
  33. package/src/features/addresses/components/AddressRecipientsList/AddressRecipientsList.tsx +9 -4
  34. package/src/features/addresses/components/AddressesCard/AddressesCard.tsx +8 -5
  35. package/src/features/addresses/components/CreateAddressDrawer/CreateAddressDrawer.tsx +27 -14
  36. package/src/features/addresses/components/CreateAddressSettingsDrawer/CreateAddressSettingsDrawer.tsx +26 -21
  37. package/src/features/addresses/components/DeleteAddressDrawer/DeleteAddressDrawer.tsx +20 -16
  38. package/src/features/addresses/components/DeleteAddressLocationDrawer/DeleteAddressLocationDrawer.tsx +26 -14
  39. package/src/features/addresses/components/DeleteRecipientAddressDrawer/DeleteRecipientAddressDrawer.tsx +20 -17
  40. package/src/features/addresses/components/EditAddressDrawer/EditAddressDrawer.tsx +12 -6
  41. package/src/features/addresses/components/EditAddressLocationDrawer/EditAddressLocationDrawer.tsx +24 -17
  42. package/src/features/addresses/components/EditRecipientAddressDrawer/EditRecipientAddressDrawer.tsx +17 -15
  43. package/src/features/addresses/components/LocationForm/LocationField/LocationField.tsx +8 -5
  44. package/src/features/addresses/components/LocationForm/LocationForm.tsx +9 -3
  45. package/src/features/addresses/components/LocationsDrawer/LocationsDrawer.tsx +15 -9
  46. package/src/features/addresses/components/RecipientsForm/RecipientItem/RecipientItem.tsx +55 -46
  47. package/src/features/addresses/components/RecipientsForm/RecipientsForm.tsx +4 -1
  48. package/src/features/addresses/components/RemoveAddressDrawer/RemoveAddressDrawer.tsx +21 -14
  49. package/src/features/addresses/layouts/AddressDetailsLayout/AddressDetailsLayout.tsx +16 -9
  50. package/src/features/addresses/layouts/AddressesLayout/AddressesLayout.tsx +11 -8
  51. package/src/features/b2b-agent/layouts/B2BAgentLayout/B2BAgentLayout.tsx +3 -1
  52. package/src/features/budgets/components/BudgetAddForm/BudgetAddForm.tsx +14 -12
  53. package/src/features/budgets/components/BudgetAddSuccess/BudgetAddSuccess.tsx +16 -14
  54. package/src/features/budgets/components/BudgetAllocationsSelection/BudgetAllocationsSelection.tsx +8 -5
  55. package/src/features/budgets/components/BudgetAllocationsTable/BudgetAllocationsTable.tsx +9 -6
  56. package/src/features/budgets/components/BudgetDeleteDrawer/BudgetDeleteDrawer.tsx +23 -17
  57. package/src/features/budgets/components/BudgetEditNotificationDrawer/BudgetEditNotificationDrawer.tsx +13 -6
  58. package/src/features/budgets/components/BudgetNotificationForm/BudgetNotificationForm.tsx +31 -24
  59. package/src/features/budgets/components/BudgetNotificationsInfo/BudgetNotificationsInfo.tsx +15 -9
  60. package/src/features/budgets/components/BudgetRemainingBalance/BudgetRemainingBalance.tsx +6 -2
  61. package/src/features/budgets/components/BudgetSettingsInfo/BudgetSettingsInfo.tsx +27 -10
  62. package/src/features/budgets/components/BudgetUsersTable/BudgetUsersTable.tsx +14 -7
  63. package/src/features/budgets/components/BudgetsTable/BudgetsTable.tsx +11 -8
  64. package/src/features/budgets/components/CreateBudgetAllocationDrawer/CreateBudgetAllocationDrawer.tsx +8 -5
  65. package/src/features/budgets/components/CreateBudgetDrawer/CreateBudgetDrawer.tsx +31 -18
  66. package/src/features/budgets/components/DeleteBudgetAllocationDrawer/DeleteBudgetAllocationDrawer.tsx +17 -12
  67. package/src/features/budgets/components/EditBudgetDrawer/EditBudgetDrawer.tsx +14 -8
  68. package/src/features/budgets/components/EmptyState.tsx/EmptyState.tsx +4 -1
  69. package/src/features/budgets/layouts/BudgetsDetailsLayout/BudgetsDetailsLayout.tsx +7 -5
  70. package/src/features/budgets/layouts/BudgetsLayout/BudgetsLayout.tsx +5 -3
  71. package/src/features/budgets/messages/budgetNotifications.ts +5 -5
  72. package/src/features/budgets/utils/handleBudgetError.tsx +5 -8
  73. package/src/features/budgets/utils/handleBudgetError.types.ts +3 -0
  74. package/src/features/buying-policies/components/AccountingFieldCriteriaSelector/AccountingFieldCriteriaSelector.tsx +5 -3
  75. package/src/features/buying-policies/components/AccountingFieldCriteriaSelector/AccountingValueCriteriaSelector.tsx +20 -9
  76. package/src/features/buying-policies/components/AddBuyingPolicyDrawer/AddBuyingPolicyDrawer.tsx +8 -5
  77. package/src/features/buying-policies/components/BasicBuyingPolicyDrawer/BasicBuyingPolicyDrawer.tsx +49 -33
  78. package/src/features/buying-policies/components/BudgetCriteriaSelector/BudgetCriteriaSelector.tsx +8 -2
  79. package/src/features/buying-policies/components/BuyingPolicyDropdownMenu/BuyingPolicyDropdownMenu.tsx +5 -3
  80. package/src/features/buying-policies/components/DeleteBuyingPolicyDrawer/DeleteBuyingPolicyDrawer.tsx +28 -24
  81. package/src/features/buying-policies/components/UpdateBuyingPolicyDrawer/UpdateBuyingPolicyDrawer.tsx +10 -7
  82. package/src/features/buying-policies/layouts/BuyingPoliciesLayout/BuyingPoliciesLayout.tsx +11 -4
  83. package/src/features/buying-policies/layouts/BuyingPolicyDetailsLayout/BuyingPolicyDetailsLayout.tsx +26 -9
  84. package/src/features/buying-policies/utils/accountingFieldCriteriaHelpers.ts +2 -2
  85. package/src/features/buying-policies/utils/buyingPoliciesWorkflowTypes.ts +3 -3
  86. package/src/features/buying-policies/utils/orderFieldsCriteriaOptions.ts +8 -8
  87. package/src/features/buying-policies/utils/spendingLimitsCriteriaOptions.ts +6 -6
  88. package/src/features/contracts/components/ContractsCard/ContractsCard.tsx +7 -5
  89. package/src/features/contracts/layouts/ContractsLayout/ContractsLayout.tsx +9 -4
  90. package/src/features/credit-cards/components/CreateCreditCardDrawer/CreateCreditCardDrawer.tsx +17 -11
  91. package/src/features/credit-cards/components/CreditCardDropdownMenu/CreditCardDropdownMenu.tsx +17 -11
  92. package/src/features/credit-cards/components/CreditCardForm/CreditCardForm.tsx +8 -5
  93. package/src/features/credit-cards/components/CreditCardSettingsDrawer/CreditCardSettingsDrawer.tsx +10 -8
  94. package/src/features/credit-cards/components/DeleteCreditCardDrawer/DeleteCreditCardDrawer.tsx +19 -14
  95. package/src/features/credit-cards/components/EditCreditCardDrawer/EditCreditCardDrawer.tsx +14 -13
  96. package/src/features/credit-cards/components/RemoveCreditCardDrawer/RemoveCreditCardDrawer.tsx +18 -15
  97. package/src/features/credit-cards/layouts/CreditCardsLayout/CreditCardLayout.tsx +10 -4
  98. package/src/features/order-entry/layouts/OrderEntryLayout.tsx +3 -1
  99. package/src/features/org-units/components/AddAllToOrgUnitDropdown/AddAllToOrgUnitDropdown.tsx +11 -9
  100. package/src/features/org-units/components/AuthSetupDrawer/AuthSetupDrawer.tsx +28 -16
  101. package/src/features/org-units/components/CreateOrgUnitDrawer/CreateOrgUnitDrawer.tsx +18 -12
  102. package/src/features/org-units/components/DeleteOrgUnitDrawer/DeleteOrgUnitDrawer.tsx +28 -29
  103. package/src/features/org-units/components/OrgUnitBreadcrumb/OrgUnitBreadcrumbLink.tsx +4 -4
  104. package/src/features/org-units/components/OrgUnitDetailsDropdownMenu/OrgUnitDetailsDropdownMenu.tsx +4 -2
  105. package/src/features/org-units/components/OrgUnitDetailsNavbar/OrgUnitDetailsNavbar.tsx +23 -5
  106. package/src/features/org-units/components/OrgUnitDetailsNavbar/org-unit-details-navbar.scss +18 -2
  107. package/src/features/org-units/components/OrgUnitsDropdownMenu/OrgUnitsDropdownMenu.tsx +6 -4
  108. package/src/features/org-units/components/OrgUnitsHierarchyTree/OrgUnitsHierarchyTree.tsx +9 -6
  109. package/src/features/org-units/components/OrganizationalUnitsCard/OrganizationalUnitsCard.tsx +15 -7
  110. package/src/features/org-units/components/UpdateOrgUnitDrawer/UpdateOrgUnitDrawer.tsx +15 -9
  111. package/src/features/org-units/layouts/OrgUnitDetailsLayout/OrgUnitDetailsLayout.tsx +27 -26
  112. package/src/features/org-units/layouts/OrgUnitsLayout/OrgUnitsLayout.tsx +8 -3
  113. package/src/features/payment-methods/components/AddPaymentMethodsDrawer/AddPaymentMethodsDrawer.tsx +29 -18
  114. package/src/features/payment-methods/components/PaymentMethodSettingsDrawer/PaymentMethodSettingsDrawer.tsx +13 -9
  115. package/src/features/payment-methods/components/RemoveMethodButton/RemoveMethodButton.tsx +5 -2
  116. package/src/features/payment-methods/components/RemovePaymentMethodsDrawer/RemovePaymentMethodsDrawer.tsx +10 -7
  117. package/src/features/payment-methods/hooks/useRemovePaymentMethodSubmit.ts +6 -4
  118. package/src/features/payment-methods/layouts/PaymentMethodsLayout/PaymentMethodsLayout.tsx +21 -15
  119. package/src/features/product-assortment/components/ProductAssortmentTable/ProductAssortmentTable.tsx +11 -7
  120. package/src/features/product-assortment/layouts/ProductAssortmentLayout/ProductAssortmentLayout.tsx +9 -6
  121. package/src/features/profile/components/ProfileCard/ProfileCard.tsx +8 -5
  122. package/src/features/profile/components/ProfileSummary/ProfileSummary.tsx +13 -6
  123. package/src/features/profile/layouts/ProfileLayout/ProfileLayout.tsx +16 -6
  124. package/src/features/roles/layout/RoleDetailsLayout/RoleDetailsLayout.tsx +4 -2
  125. package/src/features/roles/layout/RolesLayout/RolesLayout.tsx +5 -3
  126. package/src/features/shared/components/BasicCard/BasicCard.tsx +6 -4
  127. package/src/features/shared/components/BasicCard/BasicCardLine.tsx +5 -4
  128. package/src/features/shared/components/CustomerSwitch/CustomerSwitchDrawer.tsx +10 -2
  129. package/src/features/shared/components/CustomerSwitch/CustomerSwitchSearch.tsx +4 -1
  130. package/src/features/shared/components/DropdownFilter/DropdownFilter.tsx +11 -7
  131. package/src/features/shared/components/Error/Error.tsx +13 -6
  132. package/src/features/shared/components/HeaderInside/HeaderInside.tsx +5 -2
  133. package/src/features/shared/components/InternalSearch/InternalSearch.tsx +4 -1
  134. package/src/features/shared/components/InternalTopbar/InternalTopbar.tsx +7 -5
  135. package/src/features/shared/components/MainLinksDropdownMenu/MainLinksDropdownMenu.tsx +23 -8
  136. package/src/features/shared/components/OrgUnitInputSearch/OrgUnitInputSearch.tsx +7 -2
  137. package/src/features/shared/components/Pagination/Pagination.tsx +6 -2
  138. package/src/features/shared/components/Paginator/Counter.tsx +11 -5
  139. package/src/features/shared/components/Paginator/Interval.tsx +4 -3
  140. package/src/features/shared/components/Paginator/NextPageButton.tsx +6 -2
  141. package/src/features/shared/components/SettingsDrawer/SettingsDrawer.tsx +6 -4
  142. package/src/features/shared/components/SettingsDrawer/SettingsDrawerListType.tsx +11 -10
  143. package/src/features/shared/components/SettingsDrawer/listTypeOptions.ts +10 -7
  144. package/src/features/shared/components/SortFilter/SortFilter.tsx +15 -8
  145. package/src/features/shared/components/Table/TableRow/TableRow.tsx +5 -1
  146. package/src/features/shared/components/VerticalNav/VerticalNavLink.tsx +7 -3
  147. package/src/features/shared/layouts/BaseTabsLayout/Navbar.tsx +28 -4
  148. package/src/features/shared/layouts/BaseTabsLayout/about-drawer/AboutDrawer.tsx +15 -5
  149. package/src/features/shared/layouts/BaseTabsLayout/base-tabs-layout.scss +24 -9
  150. package/src/features/shared/layouts/BaseTabsLayout/sidebar-drawer/SidebarDrawer.tsx +5 -2
  151. package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +10 -5
  152. package/src/features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout.tsx +7 -4
  153. package/src/features/shared/layouts/FinanceTabsLayout/FinanceTabsLayout.tsx +9 -4
  154. package/src/features/shared/layouts/LoadingTabsLayout/LoadingTabsLayout.tsx +7 -4
  155. package/src/features/shared/layouts/OrgUnitTabsLayout/OrgUnitTabLayout.tsx +3 -0
  156. package/src/features/shared/layouts/SumaPageLayout/FullSidebarNav.tsx +13 -7
  157. package/src/features/shared/layouts/SumaPageLayout/SumaSidebar.tsx +9 -5
  158. package/src/features/shared/localization/LocalizationContext.tsx +35 -0
  159. package/src/features/shared/localization/__tests__/createLocalize.test.ts +166 -0
  160. package/src/features/shared/localization/__tests__/get-localization-labels.service.test.ts +202 -0
  161. package/src/features/shared/localization/clients/LocalizationClient.ts +19 -0
  162. package/src/features/shared/localization/constants.ts +31 -0
  163. package/src/features/shared/localization/createLocalize.ts +78 -0
  164. package/src/features/shared/localization/services/get-localization-labels.service.ts +76 -0
  165. package/src/features/shared/localization/types.ts +66 -0
  166. package/src/features/shared/localization/withLocalization.tsx +22 -0
  167. package/src/features/shared/types/AuthRouteProps.ts +5 -0
  168. package/src/features/shared/types/LoaderData.ts +1 -0
  169. package/src/features/shared/utils/addresLabelToPropMapping.ts +10 -10
  170. package/src/features/shared/utils/constants.ts +1 -4
  171. package/src/features/shared/utils/getContractSettingsLinks.ts +17 -12
  172. package/src/features/shared/utils/getFinanceSettingsLinks.ts +14 -9
  173. package/src/features/shared/utils/getKeyByValue.ts +2 -2
  174. package/src/features/shared/utils/getOrganizationSettingsLinks.ts +13 -3
  175. package/src/features/shared/utils/getSumaListingSidebarLinks.ts +15 -10
  176. package/src/features/shared/utils/index.ts +2 -0
  177. package/src/features/shared/utils/routeLayoutMapping.ts +30 -30
  178. package/src/features/shared/utils/search.tsx +10 -0
  179. package/src/features/shared/utils/withAuth.tsx +6 -0
  180. package/src/features/shared/utils/withAuthLoader.ts +11 -0
  181. package/src/features/shared/utils/withLoaderErrorBoundary.ts +9 -0
  182. package/src/features/shared/utils/withProviders.tsx +13 -2
  183. package/src/features/users/components/CreateUserDrawer/CreateUserDrawer.tsx +39 -27
  184. package/src/features/users/components/CreateUserDrawerWithUsername/CreateUserDrawerWithUsername.tsx +41 -39
  185. package/src/features/users/components/DeleteUserDrawer/DeleteUserDrawer.tsx +27 -23
  186. package/src/features/users/components/EmailTransactionalSection/EmailTransactionalSection.tsx +6 -8
  187. package/src/features/users/components/LoginField/LoginField.tsx +4 -4
  188. package/src/features/users/components/ReassignOrgUnitDrawer/ReassignOrgUnitDrawer.tsx +22 -19
  189. package/src/features/users/components/ResetPasswordDrawer/ResetPasswordDrawer.tsx +16 -21
  190. package/src/features/users/components/UpdateUserDrawer/UpdateUserDrawer.tsx +26 -15
  191. package/src/features/users/components/UpdateUserDrawerWithUsername/UpdateUserDrawerWithUsername.tsx +22 -8
  192. package/src/features/users/components/UserDropdownMenu/UserDropdownMenu.tsx +7 -5
  193. package/src/features/users/components/UserFormFields/UserFormFields.tsx +9 -8
  194. package/src/features/users/components/UsersCard/UsersCard.tsx +4 -2
  195. package/src/features/users/layouts/UserDetailsLayout/UserDetailsLayout.tsx +26 -10
  196. package/src/features/users/layouts/UsersLayout/UsersLayout.tsx +12 -8
  197. package/src/pages/accounting-field.tsx +28 -15
  198. package/src/pages/address-details.tsx +37 -26
  199. package/src/pages/addresses.tsx +27 -12
  200. package/src/pages/budgets-details.tsx +25 -15
  201. package/src/pages/budgets.tsx +31 -17
  202. package/src/pages/buyer-organization-manager.tsx +23 -12
  203. package/src/pages/buying-policies.tsx +38 -20
  204. package/src/pages/buying-policy-details.tsx +20 -19
  205. package/src/pages/contracts.tsx +15 -1
  206. package/src/pages/credit-cards.tsx +30 -17
  207. package/src/pages/order-entry.tsx +26 -15
  208. package/src/pages/org-unit-details.tsx +20 -6
  209. package/src/pages/org-units.tsx +20 -6
  210. package/src/pages/payment-groups.tsx +15 -12
  211. package/src/pages/productAssortment.tsx +10 -1
  212. package/src/pages/profile.tsx +14 -1
  213. package/src/pages/role-details.tsx +19 -6
  214. package/src/pages/roles.tsx +27 -14
  215. package/src/pages/user-details.tsx +29 -17
  216. package/src/pages/users.tsx +11 -0
  217. package/vitest.config.ts +13 -0
  218. package/src/features/addresses/constants/messages.ts +0 -8
  219. package/src/features/credit-cards/constants/messages.ts +0 -6
@@ -6,6 +6,7 @@ import { OrgUnitDetailsNavbar } from "../../../org-units/components";
6
6
  import { HeaderInside } from "../../components";
7
7
  import Error from "../../components/Error/Error";
8
8
  import { useBuyerPortal } from "../../hooks";
9
+ import { useLocalization } from "../../localization/LocalizationContext";
9
10
  import { getTabsLayoutConfigFromRoute } from "../../utils/routeLayoutMapping";
10
11
  import { ContractTabsLayout } from "../ContractTabsLayout/ContractTabsLayout";
11
12
  import { FinanceTabsLayout } from "../FinanceTabsLayout/FinanceTabsLayout";
@@ -50,6 +51,7 @@ export const ErrorTabsLayoutContent = ({
50
51
  );
51
52
 
52
53
  export const ErrorTabsLayout = ({ children, error }: ErrorTabsLayoutProps) => {
54
+ const { t } = useLocalization();
53
55
  const router = useRouter();
54
56
 
55
57
  const {
@@ -67,7 +69,8 @@ export const ErrorTabsLayout = ({ children, error }: ErrorTabsLayoutProps) => {
67
69
  return <>{children}</>;
68
70
  }
69
71
 
70
- const pageTitle = layoutConfig.pageTitle;
72
+ const pageTitle = t(layoutConfig.pageTitle);
73
+ const pageName = t(layoutConfig.pageName);
71
74
 
72
75
  switch (layoutConfig.layout) {
73
76
  case "ContractTabsLayout":
@@ -77,7 +80,7 @@ export const ErrorTabsLayout = ({ children, error }: ErrorTabsLayoutProps) => {
77
80
  orgUnitId={orgUnitId}
78
81
  contractId={customerId}
79
82
  contractName={contract?.name ?? ""}
80
- pageName="Contract"
83
+ pageName={pageName}
81
84
  >
82
85
  <ErrorTabsLayoutContent title={pageTitle} error={error} />
83
86
  </ContractTabsLayout>
@@ -85,14 +88,14 @@ export const ErrorTabsLayout = ({ children, error }: ErrorTabsLayoutProps) => {
85
88
 
86
89
  case "FinanceTabsLayout":
87
90
  return (
88
- <FinanceTabsLayout pageName={layoutConfig.pageName}>
91
+ <FinanceTabsLayout pageName={pageName}>
89
92
  <ErrorTabsLayoutContent title={pageTitle} error={error} />
90
93
  </FinanceTabsLayout>
91
94
  );
92
95
 
93
96
  case "OrgUnitTabsLayout":
94
97
  return (
95
- <OrgUnitTabsLayout pageName={layoutConfig.pageName}>
98
+ <OrgUnitTabsLayout pageName={pageName}>
96
99
  <ErrorTabsLayoutContent title={pageTitle} error={error} />
97
100
  </OrgUnitTabsLayout>
98
101
  );
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
3
  import { useBuyerPortal } from "../../hooks";
4
+ import { useLocalization } from "../../localization/LocalizationContext";
4
5
  import { getFinanceSettingsLinks } from "../../utils";
5
6
  import { BaseTabsLayout } from "../BaseTabsLayout/BaseTabsLayout";
6
7
 
@@ -15,12 +16,16 @@ export const FinanceTabsLayout = ({
15
16
  children,
16
17
  loading = false,
17
18
  }: FinanceTabsLayoutProps) => {
19
+ const { t } = useLocalization();
18
20
  const { currentOrgUnit, currentUser, currentContract } = useBuyerPortal();
19
21
 
20
- const verticalLinks = getFinanceSettingsLinks({
21
- contractId: currentContract?.id ?? "",
22
- orgUnitId: currentOrgUnit?.id ?? "",
23
- });
22
+ const verticalLinks = getFinanceSettingsLinks(
23
+ {
24
+ contractId: currentContract?.id ?? "",
25
+ orgUnitId: currentOrgUnit?.id ?? "",
26
+ },
27
+ t
28
+ );
24
29
 
25
30
  return (
26
31
  <BaseTabsLayout data-fs-bp-finance-tabs-layout>
@@ -5,6 +5,7 @@ import { Skeleton } from "@faststore/ui";
5
5
  import { OrgUnitsDetailsLayout } from "../../../org-units/layouts/OrgUnitDetailsLayout/OrgUnitDetailsLayout";
6
6
  import { HeaderInside } from "../../components";
7
7
  import { useRouterLoading } from "../../hooks";
8
+ import { useLocalization } from "../../localization/LocalizationContext";
8
9
  import {
9
10
  getTabsLayoutConfigFromRoute,
10
11
  extractRouteParams,
@@ -33,6 +34,7 @@ export const LoadingTabsLayoutContent = ({
33
34
  );
34
35
 
35
36
  export const LoadingTabsLayout = ({ children }: LoadingTabsLayoutProps) => {
37
+ const { t } = useLocalization();
36
38
  const { routerLoading, nextRoute } = useRouterLoading();
37
39
  const [shouldShowLoading, setShouldShowLoading] = useState(false);
38
40
  const wasLoadingRef = useRef(false);
@@ -63,7 +65,8 @@ export const LoadingTabsLayout = ({ children }: LoadingTabsLayoutProps) => {
63
65
 
64
66
  const routeParams = extractRouteParams(nextRoute);
65
67
 
66
- const pageTitle = layoutConfig.pageTitle;
68
+ const pageTitle = t(layoutConfig.pageTitle);
69
+ const pageName = t(layoutConfig.pageName);
67
70
 
68
71
  switch (layoutConfig.layout) {
69
72
  case "ContractTabsLayout":
@@ -73,7 +76,7 @@ export const LoadingTabsLayout = ({ children }: LoadingTabsLayoutProps) => {
73
76
  orgUnitId={routeParams.orgUnitId}
74
77
  contractName={""}
75
78
  contractId={routeParams.contractId}
76
- pageName={layoutConfig.pageName}
79
+ pageName={pageName}
77
80
  loading={true}
78
81
  >
79
82
  <LoadingTabsLayoutContent title={pageTitle} />
@@ -82,14 +85,14 @@ export const LoadingTabsLayout = ({ children }: LoadingTabsLayoutProps) => {
82
85
 
83
86
  case "FinanceTabsLayout":
84
87
  return (
85
- <FinanceTabsLayout pageName={layoutConfig.pageName} loading={true}>
88
+ <FinanceTabsLayout pageName={pageName} loading={true}>
86
89
  <LoadingTabsLayoutContent title={pageTitle} />
87
90
  </FinanceTabsLayout>
88
91
  );
89
92
 
90
93
  case "OrgUnitTabsLayout":
91
94
  return (
92
- <OrgUnitTabsLayout pageName={layoutConfig.pageName} loading={true}>
95
+ <OrgUnitTabsLayout pageName={pageName} loading={true}>
93
96
  <LoadingTabsLayoutContent title={pageTitle} />
94
97
  </OrgUnitTabsLayout>
95
98
  );
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
3
  import { useBuyerPortal } from "../../hooks";
4
+ import { useLocalization } from "../../localization/LocalizationContext";
4
5
  import { getOrganizationSettingsLinks } from "../../utils";
5
6
  import { BaseTabsLayout } from "../BaseTabsLayout/BaseTabsLayout";
6
7
 
@@ -15,11 +16,13 @@ export const OrgUnitTabsLayout = ({
15
16
  children,
16
17
  loading = false,
17
18
  }: OrgUnitTabsLayoutProps) => {
19
+ const { t } = useLocalization();
18
20
  const { currentOrgUnit, currentUser, currentContract } = useBuyerPortal();
19
21
 
20
22
  const verticalLinks = getOrganizationSettingsLinks({
21
23
  orgUnitId: currentOrgUnit?.id ?? "",
22
24
  contractId: currentContract?.id ?? "",
25
+ t,
23
26
  });
24
27
 
25
28
  return (
@@ -8,6 +8,7 @@ import { useListAccountingFields } from "../../../accounting-fields/hooks";
8
8
  import { checkAccountingFieldIsEmpty } from "../../../accounting-fields/utils";
9
9
  import { Icon, VerticalNav } from "../../components";
10
10
  import { useBuyerPortal, useDrawerProps } from "../../hooks";
11
+ import { useLocalization } from "../../localization/LocalizationContext";
11
12
  import {
12
13
  getContractSettingsLinks,
13
14
  getFinanceSettingsLinks,
@@ -40,6 +41,7 @@ export const FullSidebarNav = ({
40
41
  loading = false,
41
42
  }: FullSidebarNavProps) => {
42
43
  const { currentOrgUnit, currentContract } = useBuyerPortal();
44
+ const { t } = useLocalization();
43
45
 
44
46
  const {
45
47
  open: openAccountingFieldDrawer,
@@ -70,11 +72,12 @@ export const FullSidebarNav = ({
70
72
  { lazy: loading }
71
73
  );
72
74
 
73
- const contractMenu =
74
- getContractSettingsLinks(linkParams).map(mapLinksHandler);
75
+ const contractMenu = getContractSettingsLinks(linkParams, t).map(
76
+ mapLinksHandler
77
+ );
75
78
 
76
79
  contractMenu.push({
77
- name: "Accounting fields",
80
+ name: t("layouts.navigation.accountingFields"),
78
81
  loading: listAccountingFieldLoading,
79
82
  submenu: {
80
83
  items: (listAccountingFieldData ?? []).map((item) => ({
@@ -106,18 +109,21 @@ export const FullSidebarNav = ({
106
109
  const organizationMenu = getOrganizationSettingsLinks({
107
110
  orgUnitId: resolvedOrgUnitId,
108
111
  contractId: resolvedContractId,
112
+ t,
109
113
  });
110
- const financeMenu = getFinanceSettingsLinks(linkParams);
114
+ const financeMenu = getFinanceSettingsLinks(linkParams, t);
111
115
 
112
116
  return (
113
117
  <>
114
- <VerticalNav.Menu title={showContractTitle ? "Contract" : undefined}>
118
+ <VerticalNav.Menu
119
+ title={showContractTitle ? t("layouts.tabs.contract") : undefined}
120
+ >
115
121
  <VerticalNav.Tree items={contractMenu} />
116
122
  </VerticalNav.Menu>
117
- <VerticalNav.Menu title="Organization">
123
+ <VerticalNav.Menu title={t("layouts.tabs.organization")}>
118
124
  <VerticalNav.Tree items={organizationMenu} />
119
125
  </VerticalNav.Menu>
120
- <VerticalNav.Menu title="Finance and Compliance">
126
+ <VerticalNav.Menu title={t("layouts.tabs.financeAndCompliance")}>
121
127
  <VerticalNav.Tree items={financeMenu} />
122
128
  </VerticalNav.Menu>
123
129
 
@@ -2,6 +2,7 @@ import Link from "next/link";
2
2
 
3
3
  import { Icon, LetterHighlight, VerticalNav } from "../../components";
4
4
  import { useBuyerPortal } from "../../hooks";
5
+ import { useLocalization } from "../../localization/LocalizationContext";
5
6
  import { buyerPortalRoutes } from "../../utils/buyerPortalRoutes";
6
7
  import { getSumaListingSidebarLinks } from "../../utils/getSumaListingSidebarLinks";
7
8
 
@@ -23,6 +24,7 @@ export const SumaSidebar = ({
23
24
  loading = false,
24
25
  }: SumaSidebarProps) => {
25
26
  const { featureFlags, currentContract } = useBuyerPortal();
27
+ const { t } = useLocalization();
26
28
  const contractsUrl = buyerPortalRoutes.contracts({ orgUnitId });
27
29
 
28
30
  const contractName = currentContract?.name ?? currentContract?.email ?? "";
@@ -64,11 +66,13 @@ export const SumaSidebar = ({
64
66
 
65
67
  {variant === "listing" ? (
66
68
  <VerticalNav.Menu>
67
- {getSumaListingSidebarLinks({ orgUnitId, contractId }).map((item) => (
68
- <VerticalNav.Link key={item.link} link={item.link}>
69
- {item.name}
70
- </VerticalNav.Link>
71
- ))}
69
+ {getSumaListingSidebarLinks({ orgUnitId, contractId }, t).map(
70
+ (item) => (
71
+ <VerticalNav.Link key={item.link} link={item.link}>
72
+ {item.name}
73
+ </VerticalNav.Link>
74
+ )
75
+ )}
72
76
  </VerticalNav.Menu>
73
77
  ) : (
74
78
  <FullSidebarNav
@@ -0,0 +1,35 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+
3
+ import { createLocalize } from "./createLocalize";
4
+
5
+ import type { LabelMap, LocalizeFn } from "./types";
6
+
7
+ type LocalizationContextType = { t: LocalizeFn; labels: LabelMap };
8
+
9
+ const LocalizationContext = createContext<LocalizationContextType | undefined>(
10
+ undefined
11
+ );
12
+
13
+ export const LocalizationProvider = ({
14
+ labels,
15
+ children,
16
+ }: {
17
+ labels: LabelMap;
18
+ children: ReactNode;
19
+ }) => {
20
+ const value = useMemo<LocalizationContextType>(
21
+ () => ({ labels, t: createLocalize(labels) }),
22
+ [labels]
23
+ );
24
+ return (
25
+ <LocalizationContext.Provider value={value}>
26
+ {children}
27
+ </LocalizationContext.Provider>
28
+ );
29
+ };
30
+
31
+ export const useLocalization = (): LocalizationContextType => {
32
+ const ctx = useContext(LocalizationContext);
33
+ // Defensive fallback: t() never crashes outside a provider.
34
+ return ctx ?? { labels: {}, t: createLocalize({}) };
35
+ };
@@ -0,0 +1,166 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { createLocalize } from "../createLocalize";
6
+
7
+ describe("createLocalize", () => {
8
+ describe("US-1: static label lookup", () => {
9
+ it("returns the translated value when the key exists", () => {
10
+ const t = createLocalize({ "feature.group.name": "Group Name" });
11
+ expect(t("feature.group.name")).toBe("Group Name");
12
+ });
13
+
14
+ it("returns the key itself when the key is not found (fallback)", () => {
15
+ const t = createLocalize({});
16
+ expect(t("feature.group.name")).toBe("feature.group.name");
17
+ });
18
+
19
+ it("returns the key itself when labels map is empty", () => {
20
+ const t = createLocalize({});
21
+ expect(t("any.key")).toBe("any.key");
22
+ });
23
+ });
24
+
25
+ describe("US-2: index-based interpolation", () => {
26
+ it("substitutes all placeholders — happy path", () => {
27
+ const t = createLocalize({
28
+ "shared.pagination.label": "De {0} a {1} itens de {2}",
29
+ });
30
+ expect(t("shared.pagination.label", [1, 25, 100])).toBe(
31
+ "De 1 a 25 itens de 100"
32
+ );
33
+ });
34
+
35
+ it("leaves a placeholder untouched when the index has no interpolation value", () => {
36
+ const t = createLocalize({ msg: "Hello {0} {1}" });
37
+ expect(t("msg", ["world"])).toBe("Hello world {1}");
38
+ });
39
+
40
+ it("ignores extra interpolation values (no error thrown)", () => {
41
+ const t = createLocalize({ msg: "Hello {0}" });
42
+ expect(t("msg", ["world", "extra", "ignored"])).toBe("Hello world");
43
+ });
44
+
45
+ it("returns the template unchanged when interpolations array is empty", () => {
46
+ const t = createLocalize({ msg: "Hello {0}" });
47
+ expect(t("msg", [])).toBe("Hello {0}");
48
+ });
49
+
50
+ it("returns the template unchanged when no interpolations are passed", () => {
51
+ const t = createLocalize({ msg: "Static label" });
52
+ expect(t("msg")).toBe("Static label");
53
+ });
54
+
55
+ it("supports numeric interpolation values", () => {
56
+ const t = createLocalize({ count: "{0} items" });
57
+ expect(t("count", [42])).toBe("42 items");
58
+ });
59
+
60
+ it("falls back to the key even when interpolations are provided", () => {
61
+ const t = createLocalize({});
62
+ expect(t("missing.key", [1, 2])).toBe("missing.key");
63
+ });
64
+ });
65
+
66
+ describe("Key Scenarios coverage", () => {
67
+ it("Key scenario: error case — returns key for any.key when labels are empty", () => {
68
+ const t = createLocalize({});
69
+ expect(t("any.key")).toBe("any.key");
70
+ });
71
+
72
+ it("Key scenario: edge case — unknown.key returns itself", () => {
73
+ const t = createLocalize({ "other.key": "value" });
74
+ expect(t("unknown.key")).toBe("unknown.key");
75
+ });
76
+
77
+ it("Key scenario: placeholder without value leaves token intact", () => {
78
+ const t = createLocalize({ msg: "Hello {0} {1}" });
79
+ expect(t("msg", ["world"])).toBe("Hello world {1}");
80
+ });
81
+ });
82
+
83
+ describe("ReactNode interpolation", () => {
84
+ const node = {
85
+ type: "a",
86
+ props: { href: "/path" },
87
+ children: "link",
88
+ } as unknown as ReactNode;
89
+
90
+ it("returns an array when at least one interpolation is a ReactNode", () => {
91
+ const t = createLocalize({ msg: "Click {0} here" });
92
+ const result = t("msg", [node]);
93
+ expect(Array.isArray(result)).toBe(true);
94
+ });
95
+
96
+ it("interleaves text segments and nodes in order", () => {
97
+ const t = createLocalize({ msg: "Click {0} here" });
98
+ expect(t("msg", [node])).toEqual(["Click ", node, " here"]);
99
+ });
100
+
101
+ it("handles a node at the very start of the template", () => {
102
+ const t = createLocalize({ msg: "{0} was deleted" });
103
+ expect(t("msg", [node])).toEqual([node, " was deleted"]);
104
+ });
105
+
106
+ it("handles a node at the very end of the template", () => {
107
+ const t = createLocalize({ msg: "Delete {0}" });
108
+ expect(t("msg", [node])).toEqual(["Delete ", node]);
109
+ });
110
+
111
+ it("supports multiple ReactNode placeholders in one template", () => {
112
+ const node1 = {
113
+ type: "strong",
114
+ props: { key: "email" },
115
+ children: "email",
116
+ } as unknown as ReactNode;
117
+ const node2 = {
118
+ type: "strong",
119
+ props: { key: "user" },
120
+ children: "user",
121
+ } as unknown as ReactNode;
122
+ const t = createLocalize({
123
+ msg: "The email {0} belongs to {1}.",
124
+ });
125
+ expect(t("msg", [node1, node2])).toEqual([
126
+ "The email ",
127
+ node1,
128
+ " belongs to ",
129
+ node2,
130
+ ".",
131
+ ]);
132
+ });
133
+
134
+ it("allows mixing ReactNode and string interpolations", () => {
135
+ // String values in a mixed array are treated as individual ReactNode
136
+ // segments — each is its own array element, not concatenated.
137
+ const t = createLocalize({ msg: "Add {0} to {1} from {2}." });
138
+ expect(t("msg", [node, "OrgA", "OrgB"])).toEqual([
139
+ "Add ",
140
+ node,
141
+ " to ",
142
+ "OrgA",
143
+ " from ",
144
+ "OrgB",
145
+ ".",
146
+ ]);
147
+ });
148
+
149
+ it("leaves a placeholder token intact when the index has no value", () => {
150
+ // The text before the missing placeholder and the placeholder token
151
+ // itself become separate array elements.
152
+ const t = createLocalize({ msg: "Hello {0} and {1}" });
153
+ expect(t("msg", [node])).toEqual(["Hello ", node, " and ", "{1}"]);
154
+ });
155
+
156
+ it("falls back to key (string) even when ReactNode interpolations are provided", () => {
157
+ const t = createLocalize({});
158
+ expect(t("missing.key", [node])).toBe("missing.key");
159
+ });
160
+
161
+ it("returns the template string as a single-element array when no placeholders exist", () => {
162
+ const t = createLocalize({ msg: "Static label" });
163
+ expect(t("msg", [node])).toEqual(["Static label"]);
164
+ });
165
+ });
166
+ });
@@ -0,0 +1,202 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ // Mock LocalizationClient before any service import
4
+ vi.mock("../clients/LocalizationClient", () => ({
5
+ localizationClient: { getEntry: vi.fn() },
6
+ }));
7
+
8
+ import * as loggerModule from "../../services/logger";
9
+ import { localizationClient } from "../clients/LocalizationClient";
10
+ import { LOCALIZATION_ENTRY_IDS } from "../constants";
11
+ import {
12
+ _resetLocalizationCache,
13
+ getLocalizationLabelsService,
14
+ } from "../services/get-localization-labels.service";
15
+
16
+ const mockGetEntry = vi.mocked(localizationClient.getEntry);
17
+
18
+ const makeApiResponse = (labels: Array<{ key: string; value: string }>) => ({
19
+ locale_metadata: {
20
+ code: "en",
21
+ locale: "en-US",
22
+ language: {
23
+ name: "English",
24
+ language_code: "en",
25
+ native_name: "English",
26
+ script: "Latin",
27
+ text_direction: "ltr",
28
+ utf_encoding: "UTF-8",
29
+ },
30
+ region: {
31
+ name: "United States",
32
+ flag: "🇺🇸",
33
+ country_code: "US",
34
+ continent: "North America",
35
+ date_format: "MM/DD/YYYY",
36
+ time_format: "12h",
37
+ time_format_mask: "hh:mm A",
38
+ unit_system: "imperial",
39
+ default_timezone: "America/New_York",
40
+ },
41
+ },
42
+ labels,
43
+ });
44
+
45
+ describe("getLocalizationLabelsService", () => {
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ let logErrorSpy: any;
48
+
49
+ beforeEach(() => {
50
+ _resetLocalizationCache();
51
+ vi.clearAllMocks();
52
+ logErrorSpy = vi
53
+ .spyOn(loggerModule, "logError")
54
+ .mockResolvedValue(undefined);
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.restoreAllMocks();
59
+ });
60
+
61
+ describe("US-3: server-side label fetching", () => {
62
+ it("transforms the labels array into a flat key-value map (O(1) lookups)", async () => {
63
+ mockGetEntry.mockResolvedValue(
64
+ makeApiResponse([
65
+ { key: "org-units.title", value: "Org Units" },
66
+ { key: "org-units.empty", value: "No units found" },
67
+ ])
68
+ );
69
+
70
+ const result = await getLocalizationLabelsService({
71
+ entryIds: LOCALIZATION_ENTRY_IDS.orgUnits,
72
+ });
73
+
74
+ expect(result["org-units.title"]).toBe("Org Units");
75
+ expect(result["org-units.empty"]).toBe("No units found");
76
+ });
77
+
78
+ it("always merges the shared entry automatically", async () => {
79
+ mockGetEntry
80
+ .mockResolvedValueOnce(
81
+ makeApiResponse([{ key: "shared.save", value: "Save" }])
82
+ )
83
+ .mockResolvedValueOnce(
84
+ makeApiResponse([{ key: "org-units.title", value: "Org Units" }])
85
+ );
86
+
87
+ const result = await getLocalizationLabelsService({
88
+ entryIds: LOCALIZATION_ENTRY_IDS.orgUnits,
89
+ });
90
+
91
+ expect(result["shared.save"]).toBe("Save");
92
+ expect(result["org-units.title"]).toBe("Org Units");
93
+ });
94
+
95
+ it("does not call the endpoint twice for shared when it is explicitly passed too", async () => {
96
+ mockGetEntry.mockResolvedValue(
97
+ makeApiResponse([{ key: "shared.save", value: "Save" }])
98
+ );
99
+
100
+ await getLocalizationLabelsService({
101
+ entryIds: [
102
+ LOCALIZATION_ENTRY_IDS.shared,
103
+ LOCALIZATION_ENTRY_IDS.shared,
104
+ ],
105
+ });
106
+
107
+ // De-duplication: only one call despite passing shared twice
108
+ expect(mockGetEntry).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ it("accepts an array of entry IDs and merges all maps", async () => {
112
+ mockGetEntry
113
+ .mockResolvedValueOnce(
114
+ makeApiResponse([{ key: "shared.save", value: "Save" }])
115
+ )
116
+ .mockResolvedValueOnce(
117
+ makeApiResponse([{ key: "users.title", value: "Users" }])
118
+ )
119
+ .mockResolvedValueOnce(
120
+ makeApiResponse([{ key: "org-units.title", value: "Org Units" }])
121
+ );
122
+
123
+ const result = await getLocalizationLabelsService({
124
+ entryIds: [
125
+ LOCALIZATION_ENTRY_IDS.users,
126
+ LOCALIZATION_ENTRY_IDS.orgUnits,
127
+ ],
128
+ });
129
+
130
+ expect(result["shared.save"]).toBe("Save");
131
+ expect(result["users.title"]).toBe("Users");
132
+ expect(result["org-units.title"]).toBe("Org Units");
133
+ });
134
+
135
+ it("returns an empty map and logs an error when the fetch fails (graceful fallback)", async () => {
136
+ const fetchError = new Error("Network error");
137
+ mockGetEntry.mockRejectedValue(fetchError);
138
+
139
+ const result = await getLocalizationLabelsService({
140
+ entryIds: LOCALIZATION_ENTRY_IDS.orgUnits,
141
+ });
142
+
143
+ expect(result).toEqual({});
144
+ expect(logErrorSpy).toHaveBeenCalledWith(
145
+ "Failed to fetch localization labels",
146
+ expect.objectContaining({
147
+ error_message: "Network error",
148
+ })
149
+ );
150
+ });
151
+
152
+ it("does not throw even when the fetch fails", async () => {
153
+ mockGetEntry.mockRejectedValue(new Error("5xx"));
154
+
155
+ await expect(
156
+ getLocalizationLabelsService({
157
+ entryIds: LOCALIZATION_ENTRY_IDS.orgUnits,
158
+ })
159
+ ).resolves.toEqual({});
160
+ });
161
+
162
+ it("returns an empty map and logs an error for non-Error rejections", async () => {
163
+ mockGetEntry.mockRejectedValue("string-error");
164
+
165
+ const result = await getLocalizationLabelsService({
166
+ entryIds: LOCALIZATION_ENTRY_IDS.orgUnits,
167
+ });
168
+
169
+ expect(result).toEqual({});
170
+ expect(logErrorSpy).toHaveBeenCalledWith(
171
+ "Failed to fetch localization labels",
172
+ expect.objectContaining({ error_message: "string-error" })
173
+ );
174
+ });
175
+
176
+ it("retries the fetch on the next call after a transient failure (no sticky empty cache)", async () => {
177
+ // First call: both entries (shared + orgUnits) fail
178
+ mockGetEntry
179
+ .mockRejectedValueOnce(new Error("transient error")) // shared
180
+ .mockRejectedValueOnce(new Error("transient error")); // orgUnits
181
+
182
+ const first = await getLocalizationLabelsService({
183
+ entryIds: LOCALIZATION_ENTRY_IDS.orgUnits,
184
+ });
185
+ expect(first).toEqual({});
186
+
187
+ // Second call: both entries succeed — must NOT return the cached empty fallback
188
+ mockGetEntry
189
+ .mockResolvedValueOnce(
190
+ makeApiResponse([{ key: "shared.save", value: "Save" }])
191
+ )
192
+ .mockResolvedValueOnce(
193
+ makeApiResponse([{ key: "org-units.title", value: "Org Units" }])
194
+ );
195
+
196
+ const second = await getLocalizationLabelsService({
197
+ entryIds: LOCALIZATION_ENTRY_IDS.orgUnits,
198
+ });
199
+ expect(second["org-units.title"]).toBe("Org Units");
200
+ });
201
+ });
202
+ });
@@ -0,0 +1,19 @@
1
+ import { Client } from "../../clients/Client";
2
+ import { LOCALIZATION_BASE_URL } from "../constants";
3
+
4
+ import type { LocalizationLocaleDataType } from "../types";
5
+
6
+ export class LocalizationClient extends Client {
7
+ constructor() {
8
+ super(LOCALIZATION_BASE_URL);
9
+ }
10
+
11
+ getEntry(entryId: string, locale: string = "") {
12
+ return this.get<LocalizationLocaleDataType>(`/entries/${entryId}`, {
13
+ params: { locale },
14
+ componentName: "LocalizationClient",
15
+ });
16
+ }
17
+ }
18
+
19
+ export const localizationClient = new LocalizationClient();