@controleonline/ui-common 1.2.71 → 1.2.75

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 (150) hide show
  1. package/.github/actions/workers/manager/action.yml +147 -0
  2. package/.github/actions/workers/qa/action.yml +44 -0
  3. package/.github/actions/workers/security/action.yml +44 -0
  4. package/.github/actions/workers/technical-documenter/action.yml +51 -0
  5. package/.github/workflows/manager-worker.yml +168 -0
  6. package/.github/workflows/publish-npm.yml +45 -0
  7. package/.gitmodules +3 -0
  8. package/AGENTS.md +6 -0
  9. package/README.md +14 -0
  10. package/package.json +3 -2
  11. package/src/api/fetch.js +1 -1
  12. package/src/api/index.js +92 -22
  13. package/src/react/components/AddImportModal.js +94 -110
  14. package/src/react/components/BottomNavigationBar.config.js +211 -231
  15. package/src/react/components/CategoryForm.js +52 -35
  16. package/src/react/components/DefaultProvider.native.js +131 -95
  17. package/src/react/components/DefaultProvider.web.js +148 -74
  18. package/src/react/components/NetworkPrinterPreviewModal.js +249 -0
  19. package/src/react/components/NetworkPrinterPreviewModal.styles.js +159 -0
  20. package/src/react/components/RuntimeInfoFooter.js +100 -36
  21. package/src/react/components/RuntimeInfoFooter.styles.js +3 -0
  22. package/src/react/components/UnifiedPaymentBar.js +7 -4
  23. package/src/react/components/UserAvatar.js +160 -0
  24. package/src/react/components/integrations/IntegrationHero/index.js +48 -0
  25. package/src/react/components/integrations/IntegrationHero/styles.js +57 -0
  26. package/src/react/components/integrations/IntegrationTabs/index.js +89 -0
  27. package/src/react/components/integrations/IntegrationTabs/styles.js +44 -0
  28. package/src/react/config/deviceConfigBootstrap.js +5 -3
  29. package/src/react/pages/Connections.js +229 -0
  30. package/src/react/pages/Connections.styles.js +222 -0
  31. package/src/react/pages/DeviceDetailPage.js +2814 -0
  32. package/src/react/pages/DeviceDetailPage.styles.js +308 -0
  33. package/src/react/pages/Devices/currentDevice.js +194 -0
  34. package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +257 -0
  35. package/src/react/pages/Devices/deviceTypes/all.js +23 -0
  36. package/src/react/pages/Devices/deviceTypes/device.js +25 -0
  37. package/src/react/pages/Devices/deviceTypes/deviceListHelpers.js +178 -0
  38. package/src/react/pages/Devices/deviceTypes/display.js +26 -0
  39. package/src/react/pages/Devices/deviceTypes/index.js +145 -0
  40. package/src/react/pages/Devices/deviceTypes/ipCamera.js +35 -0
  41. package/src/react/pages/Devices/deviceTypes/pdv.js +88 -0
  42. package/src/react/pages/Devices/deviceTypes/printer.js +37 -0
  43. package/src/react/pages/Devices/deviceTypes/shared.js +453 -0
  44. package/src/react/pages/Devices/deviceTypes/useDeviceNetworkConnectivity.js +145 -0
  45. package/src/react/pages/Devices.js +183 -0
  46. package/src/react/pages/Devices.styles.js +353 -0
  47. package/src/react/pages/Food99IntegrationPage/components/Food99CatalogTab.js +219 -0
  48. package/src/react/pages/Food99IntegrationPage/components/Food99OverviewTab.js +134 -0
  49. package/src/react/pages/Food99IntegrationPage/components/Food99PreviewModal.js +173 -0
  50. package/src/react/pages/Food99IntegrationPage/components/Food99ProductCard.js +1 -0
  51. package/src/react/pages/Food99IntegrationPage/components/Food99QuickWalletModal.js +181 -0
  52. package/src/react/pages/Food99IntegrationPage/components/Food99SettingsTab.js +306 -0
  53. package/src/react/pages/Food99IntegrationPage/components/Food99StoreTab.js +417 -0
  54. package/src/react/pages/Food99IntegrationPage/index.js +1530 -0
  55. package/src/react/pages/Food99IntegrationPage/styles.js +792 -0
  56. package/src/react/pages/Food99IntegrationPage/utils.js +155 -0
  57. package/src/react/pages/Food99IntegrationPage.js +1 -0
  58. package/src/react/pages/Food99IntegrationPage.styles.js +1 -0
  59. package/src/react/pages/GenericLogPage.js +42 -167
  60. package/src/react/pages/IFoodIntegrationPage/components/IFoodCatalogTab.js +8 -0
  61. package/src/react/pages/IFoodIntegrationPage/components/IFoodOperationsTab.js +420 -0
  62. package/src/react/pages/IFoodIntegrationPage/components/IFoodOverviewTab.js +118 -0
  63. package/src/react/pages/IFoodIntegrationPage/components/IFoodPreviewModal.js +103 -0
  64. package/src/react/pages/IFoodIntegrationPage/components/IFoodProductCard.js +1 -0
  65. package/src/react/pages/IFoodIntegrationPage/components/IFoodStoreTab.js +150 -0
  66. package/src/react/pages/IFoodIntegrationPage/index.js +1298 -0
  67. package/src/react/pages/IFoodIntegrationPage/styles.js +764 -0
  68. package/src/react/pages/IFoodIntegrationPage/utils.js +91 -0
  69. package/src/react/pages/IFoodIntegrationPage.js +1 -0
  70. package/src/react/pages/IFoodIntegrationPage.styles.js +1 -0
  71. package/src/react/pages/Imports.js +214 -166
  72. package/src/react/pages/IntegrationConfigPage.js +744 -0
  73. package/src/react/pages/IntegrationConfigPage.styles.js +269 -0
  74. package/src/react/pages/Integrations.js +299 -0
  75. package/src/react/pages/Integrations.styles.js +130 -0
  76. package/src/react/pages/ManagerOrderNotificationsPage.js +642 -0
  77. package/src/react/pages/MarketplaceIntegrationPage/components/MarketplaceProductCard.js +205 -0
  78. package/src/react/pages/MarketplaceIntegrationPage/components/RemoteMarketplaceMenuTab.js +335 -0
  79. package/src/react/pages/MarketplaceIntegrationPage/index.js +60 -0
  80. package/src/react/pages/MarketplaceIntegrationPage/utils.js +137 -0
  81. package/src/react/pages/MercadoLivreIntegrationPage.js +538 -0
  82. package/src/react/pages/PrinterDeviceDetailPage.js +1916 -0
  83. package/src/react/pages/PrinterDeviceDetailPage.styles.js +223 -0
  84. package/src/react/pages/PrinterDeviceFormPage.js +521 -0
  85. package/src/react/pages/PrinterDeviceFormPage.styles.js +159 -0
  86. package/src/react/pages/WhatsAppConnectionPage.js +459 -0
  87. package/src/react/pages/WhatsAppConnectionPage.styles.js +284 -0
  88. package/src/react/pages/integrationsCatalog.js +399 -0
  89. package/src/react/services/Cielo/Cielo.js +193 -193
  90. package/src/react/utils/categoryContexts.js +46 -0
  91. package/src/react/utils/entityDisplay.js +222 -217
  92. package/src/react/utils/fileUrl.js +39 -20
  93. package/src/react/utils/integrationPage.js +16 -0
  94. package/src/react/utils/runtimeFooter.js +40 -22
  95. package/src/react/utils/runtimeLanguage.js +50 -9
  96. package/src/react/utils/runtimeMenu.js +191 -131
  97. package/src/react/utils/runtimeZoom.js +43 -0
  98. package/src/react/utils/shopConfig.js +27 -15
  99. package/src/react/utils/shopFranchises.js +126 -1
  100. package/src/react/utils/storeColumns.js +23 -4
  101. package/src/react/utils/userAvatar.js +104 -0
  102. package/src/store/categories/index.js +48 -20
  103. package/src/store/device_config/index.js +62 -21
  104. package/src/store/entity_log/index.js +22 -1
  105. package/src/store/imports/customActions.js +51 -0
  106. package/src/store/imports/index.js +89 -88
  107. package/src/store/status/index.js +11 -11
  108. package/src/store/themes/index.js +22 -0
  109. package/src/store/timezones/index.js +3 -1
  110. package/src/tests/browser/manager/connections.spec.js +143 -0
  111. package/src/tests/browser/manager/devices-current.spec.js +343 -0
  112. package/src/tests/browser/manager/mercado-livre-integration.spec.js +268 -0
  113. package/src/tests/react/api/deviceHeaders.test.js +114 -0
  114. package/src/tests/react/components/AddImportModal.test.js +166 -0
  115. package/src/tests/react/components/BottomNavigationBar.config.test.js +20 -0
  116. package/src/tests/react/components/UserAvatar.test.js +52 -0
  117. package/src/tests/react/pages/DevicePickerStyles.test.js +47 -0
  118. package/src/tests/react/pages/Devices/deviceListHelpers.test.js +39 -0
  119. package/src/tests/react/pages/DevicesCurrentDevice.test.js +198 -0
  120. package/src/tests/react/pages/connectionsHelpers.test.js +34 -0
  121. package/src/tests/react/pages/mercadoLivreIntegrationPageContract.test.mjs +16 -0
  122. package/src/tests/react/services/Cielo.test.js +190 -190
  123. package/src/tests/react/store/categoriesOrdering.test.js +26 -0
  124. package/src/tests/react/utils/appDomain.test.js +62 -0
  125. package/src/tests/react/utils/entityDisplay.test.js +2 -2
  126. package/src/tests/react/utils/fileUrl.test.js +3 -3
  127. package/src/tests/react/utils/formatter.test.mjs +33 -0
  128. package/src/tests/react/utils/runtimeFooter.test.js +29 -4
  129. package/src/tests/react/utils/runtimeLanguage.test.js +79 -9
  130. package/src/tests/react/utils/runtimeMenu.test.js +198 -134
  131. package/src/tests/react/utils/runtimeZoom.test.js +38 -0
  132. package/src/tests/react/utils/shopConfig.test.js +10 -9
  133. package/src/tests/react/utils/shopFranchises.test.js +43 -0
  134. package/src/tests/react/utils/translate.discovery.test.mjs +170 -0
  135. package/src/tests/react/utils/translate.test.helpers.mjs +152 -0
  136. package/src/tests/react/utils/translate.test.mjs +141 -349
  137. package/src/tests/react/utils/userAvatar.test.js +63 -0
  138. package/src/utils/appDomain.js +15 -4
  139. package/src/utils/formatter.js +104 -16
  140. package/src/utils/integrationConfigs.js +80 -80
  141. package/src/utils/translate.js +222 -438
  142. package/src/utils/translateDiscovery.js +96 -0
  143. package/src/utils/translateNormalize.js +33 -0
  144. package/src/utils/translatePending.js +92 -0
  145. package/src/utils/translateStorage.js +142 -0
  146. package/.github/agents/developer.agent.md +0 -30
  147. package/.github/agents/devops.agent.md +0 -30
  148. package/.github/agents/qa.agent.md +0 -30
  149. package/.github/agents/security.agent.md +0 -30
  150. package/.github/workflows/npmjs.yml +0 -35
@@ -1,12 +1,40 @@
1
- const TRANSLATES_STORAGE_KEY = "translates";
1
+ import {
2
+ formatMessage,
3
+ isNonEmptyMessage,
4
+ normalizeId,
5
+ normalizeLanguageCode,
6
+ resolveReferenceId,
7
+ } from "./translateNormalize.js";
8
+ import {
9
+ TRANSLATES_STORAGE_KEY,
10
+ collectCompaniesToCache,
11
+ getCompanyBucket as readCompanyBucket,
12
+ getLanguageBucket as readLanguageBucket,
13
+ getStoreBucket as readStoreBucket,
14
+ loadStorageObject,
15
+ writeMessageToBucket,
16
+ } from "./translateStorage.js";
17
+ import {
18
+ getPendingMessages as readPendingMessages,
19
+ getQueuedTranslateGroups as readQueuedGroups,
20
+ getQueuedTranslateGroupsForStore as readQueuedGroupsForStore,
21
+ hasPendingTranslate as readHasPendingTranslate,
22
+ } from "./translatePending.js";
23
+ import {
24
+ discoveryAll as runDiscoveryAll,
25
+ discoveryStoreTranslate as runDiscoveryStoreTranslate,
26
+ fetchTranslates as runFetchTranslates,
27
+ resolveStoreTranslates as runResolveStoreTranslates,
28
+ } from "./translateDiscovery.js";
2
29
 
3
30
  export default class Translate {
4
31
  constructor(companies, defaultCompany, currentCompany, stores, translateStore) {
5
- this.translates = this.loadStorageObject(TRANSLATES_STORAGE_KEY);
32
+ this.translates = loadStorageObject(TRANSLATES_STORAGE_KEY);
6
33
 
7
- this.language = this.normalizeLanguageCode(
8
- JSON.parse(localStorage.getItem("config") || "{}").language
9
- ) || "pt-br";
34
+ this.language =
35
+ normalizeLanguageCode(
36
+ JSON.parse(localStorage.getItem("config") || "{}").language,
37
+ ) || "pt-br";
10
38
 
11
39
  this.defaultCompany = defaultCompany;
12
40
  this.currentCompany = currentCompany;
@@ -17,6 +45,7 @@ export default class Translate {
17
45
  this.bootstrapStores = new Set(this.getStoreList());
18
46
  this.discoveredStores = new Set();
19
47
  this.pendingStoreDiscoveries = new Map();
48
+ this.pendingMissingTranslateSchedules = new Set();
20
49
  this.pendingTranslateResolutionPromise = null;
21
50
  this.pendingPersistRequests = new Map();
22
51
  this.t = this.t.bind(this);
@@ -27,194 +56,101 @@ export default class Translate {
27
56
  this.hydrateDiscoveredStores();
28
57
  }
29
58
 
30
- loadStorageObject(key) {
31
- if (typeof localStorage === "undefined") {
32
- return {};
33
- }
59
+ normalizeId(value) {
60
+ return normalizeId(value);
61
+ }
34
62
 
35
- try {
36
- const value = localStorage.getItem(key);
37
- if (!value) return {};
63
+ normalizeLanguageCode(value) {
64
+ return normalizeLanguageCode(value);
65
+ }
38
66
 
39
- const parsed = JSON.parse(value);
40
- return parsed && typeof parsed === "object" ? parsed : {};
41
- } catch {
42
- return {};
43
- }
67
+ formatMessage(key) {
68
+ return formatMessage(key);
44
69
  }
45
-
46
- getLanguageBucket(language = this.language, createIfMissing = false) {
47
- const normalizedLanguage = this.normalizeLanguageCode(language);
48
- if (!normalizedLanguage) return null;
49
70
 
50
- if (!this.translates[normalizedLanguage] && createIfMissing) {
51
- this.translates[normalizedLanguage] = {};
71
+ resolveReferenceId(value) {
72
+ if (value == null) return null;
73
+ if (typeof value === "object") {
74
+ return this.normalizeId(
75
+ value.id || value["@id"] || value.people || value.language,
76
+ );
52
77
  }
78
+ return this.normalizeId(value);
79
+ }
53
80
 
54
- return this.translates[normalizedLanguage] || null;
81
+ getLanguageBucket(language = this.language, createIfMissing = false) {
82
+ return readLanguageBucket(this.translates, language, createIfMissing);
55
83
  }
56
84
 
57
85
  getCompanyBucket(companyId, createIfMissing = false, language = this.language) {
58
- const normalizedCompanyId = this.normalizeId(companyId);
59
- if (!normalizedCompanyId) return null;
60
-
61
- const languageBucket = this.getLanguageBucket(language, createIfMissing);
62
- if (!languageBucket) return null;
63
-
64
- if (!languageBucket.companies && createIfMissing) {
65
- languageBucket.companies = {};
66
- }
67
-
68
- if (
69
- createIfMissing &&
70
- languageBucket.companies &&
71
- !languageBucket.companies[normalizedCompanyId]
72
- ) {
73
- languageBucket.companies[normalizedCompanyId] = {};
74
- }
75
-
76
- return languageBucket.companies?.[normalizedCompanyId] || null;
86
+ return readCompanyBucket(
87
+ this.translates,
88
+ companyId,
89
+ createIfMissing,
90
+ language,
91
+ );
77
92
  }
78
93
 
79
94
  getStoreBucket(companyId, store, createIfMissing = false, language = this.language) {
80
- if (!store) return null;
81
-
82
- if (companyId) {
83
- const companyBucket = this.getCompanyBucket(companyId, createIfMissing, language);
84
- if (!companyBucket) return null;
85
-
86
- if (createIfMissing && !companyBucket[store]) {
87
- companyBucket[store] = {};
88
- }
89
-
90
- return companyBucket[store] || null;
91
- }
92
-
93
- const languageBucket = this.getLanguageBucket(language, createIfMissing);
94
- if (!languageBucket) return null;
95
-
96
- if (createIfMissing && !languageBucket[store]) {
97
- languageBucket[store] = {};
98
- }
99
-
100
- return languageBucket[store] || null;
101
- }
102
-
103
- normalizeId(value) {
104
- if (value == null) return null;
105
-
106
- const match = String(value).match(/\d+/);
107
- return match?.[0] || null;
108
- }
109
-
110
- normalizeLanguageCode(value) {
111
- if (typeof value !== "string") return "";
112
-
113
- return value.trim().replace(/_/g, "-").toLowerCase();
114
- }
115
-
116
- getCompaniesToCache() {
117
- const candidates = [
118
- this.defaultCompany,
119
- this.currentCompany,
120
- ].filter((company) => company?.id);
121
-
122
- const unique = [];
123
- const seen = new Set();
124
-
125
- candidates.forEach((company) => {
126
- const companyId = this.normalizeId(company.id);
127
- if (!companyId || seen.has(companyId)) return;
128
-
129
- seen.add(companyId);
130
- unique.push(company);
131
- });
132
-
133
- return unique;
134
- }
135
-
136
- getStoreList() {
137
- if (Array.isArray(this.stores)) {
138
- return this.stores.filter(Boolean);
139
- }
140
-
141
- return this.stores ? [this.stores] : [];
95
+ return readStoreBucket(
96
+ this.translates,
97
+ companyId,
98
+ store,
99
+ createIfMissing,
100
+ language,
101
+ );
142
102
  }
143
103
 
144
- getPendingMessages() {
145
- const messages = this.translateStore?.getters?.pendingMessages;
146
- if (!messages || typeof messages !== "object" || Array.isArray(messages)) {
147
- return {};
148
- }
149
-
150
- return messages;
104
+ getCompaniesToCache() {
105
+ return collectCompaniesToCache(this.defaultCompany, this.currentCompany);
151
106
  }
152
107
 
153
- getPendingLanguageBucket(language = this.language) {
154
- const pendingMessages = this.getPendingMessages();
155
- const normalizedLanguage = this.normalizeLanguageCode(language);
156
- if (!normalizedLanguage) return null;
157
-
158
- return pendingMessages[normalizedLanguage] || null;
108
+ // Always load current + main company so runtime resolution can apply
109
+ // current company -> main company -> global fallback (getMessageFromBuckets).
110
+ getResolveCompaniesToCache() {
111
+ return this.getCompaniesToCache();
159
112
  }
160
113
 
161
- getPendingCompanyBucket(companyId, language = this.language) {
162
- const normalizedCompanyId = this.normalizeId(companyId);
163
- if (!normalizedCompanyId) return null;
164
-
165
- const languageBucket = this.getPendingLanguageBucket(language);
166
- if (!languageBucket) return null;
167
-
168
- const companiesBucket = languageBucket.companies;
169
- if (!companiesBucket || typeof companiesBucket !== "object") {
170
- return null;
114
+ getStoreList() {
115
+ if (Array.isArray(this.stores)) {
116
+ return this.stores.filter(Boolean);
171
117
  }
172
-
173
- return companiesBucket[normalizedCompanyId] || null;
118
+ return this.stores ? [this.stores] : [];
174
119
  }
175
120
 
176
- getPendingStoreBucket(companyId, store, language = this.language) {
177
- const normalizedStore = String(store || "").trim();
178
- if (!normalizedStore) return null;
179
-
180
- const companyBucket = this.getPendingCompanyBucket(companyId, language);
181
- if (!companyBucket) return null;
182
-
183
- return companyBucket[normalizedStore] || null;
121
+ getPendingMessages() {
122
+ return readPendingMessages(this.translateStore);
184
123
  }
185
124
 
186
125
  hasPendingTranslate(store, type, key) {
187
- const pendingStoreBucket = this.getPendingStoreBucket(
126
+ return readHasPendingTranslate(
127
+ this.translateStore,
188
128
  this.defaultCompany?.id,
189
129
  store,
130
+ type,
131
+ key,
190
132
  this.language,
191
133
  );
192
-
193
- return Boolean(pendingStoreBucket?.[type]?.[key]);
194
134
  }
195
135
 
196
136
  getStoreDiscoveryToken(store) {
197
- return [
198
- this.language,
199
- String(store || ""),
200
- ].join("::");
137
+ return `discover:${store}:${this.language}`;
201
138
  }
202
139
 
203
140
  canDiscoverStore() {
204
- return typeof this.translateActions?.getItems === "function";
141
+ return typeof this.translateActions?.resolveQueuedMessages === "function";
205
142
  }
206
143
 
207
144
  hasCachedBootstrapStore(store) {
208
- if (!store) return false;
209
-
210
145
  const companies = this.getCompaniesToCache();
211
146
  if (companies.length === 0) return false;
212
-
213
- return companies.every((company) => this.getStoreBucket(company.id, store) != null);
147
+ return companies.every(
148
+ (company) => this.getStoreBucket(company.id, store) != null,
149
+ );
214
150
  }
215
151
 
216
152
  hydrateDiscoveredStores() {
217
- this.bootstrapStores.forEach((store) => {
153
+ this.getStoreList().forEach((store) => {
218
154
  if (this.hasCachedBootstrapStore(store)) {
219
155
  this.markStoreDiscovered(store);
220
156
  }
@@ -222,96 +158,55 @@ export default class Translate {
222
158
  }
223
159
 
224
160
  hasDiscoveredStore(store) {
225
- if (!store) return false;
226
-
227
- const token = this.getStoreDiscoveryToken(store);
228
- if (this.discoveredStores.has(token)) {
229
- return true;
230
- }
231
-
232
- if (this.hasCachedBootstrapStore(store)) {
233
- this.markStoreDiscovered(store);
234
- return true;
235
- }
236
-
237
- return false;
161
+ return this.discoveredStores.has(store);
238
162
  }
239
163
 
240
164
  markStoreDiscovered(store) {
241
- if (!store) return;
242
-
243
- this.discoveredStores.add(this.getStoreDiscoveryToken(store));
165
+ if (store) this.discoveredStores.add(store);
244
166
  }
245
167
 
246
168
  ensureStoreDiscovered(store) {
247
- if (!store || !this.canDiscoverStore()) {
169
+ if (!store || this.hasDiscoveredStore(store) || !this.canDiscoverStore()) {
248
170
  return Promise.resolve(this.translates);
249
171
  }
250
172
 
251
- if (this.hasDiscoveredStore(store)) {
252
- return Promise.resolve(this.translates);
173
+ if (this.pendingStoreDiscoveries.has(store)) {
174
+ return this.pendingStoreDiscoveries.get(store);
253
175
  }
254
176
 
255
- const token = this.getStoreDiscoveryToken(store);
256
- if (this.pendingStoreDiscoveries.has(token)) {
257
- return this.pendingStoreDiscoveries.get(token);
258
- }
259
-
260
- const discoveryPromise = Promise.resolve()
261
- .then(() => this.discoveryStoreTranslate(store))
177
+ const discoveryPromise = this.discoveryStoreTranslate(store)
178
+ .catch(() => this.translates)
262
179
  .finally(() => {
263
- this.pendingStoreDiscoveries.delete(token);
180
+ this.pendingStoreDiscoveries.delete(store);
264
181
  });
265
182
 
266
- this.pendingStoreDiscoveries.set(token, discoveryPromise);
267
-
183
+ this.pendingStoreDiscoveries.set(store, discoveryPromise);
268
184
  return discoveryPromise;
269
185
  }
270
186
 
271
187
  notifyTranslationsUpdated() {
272
- const globalObject =
273
- typeof global !== "undefined" ? global : globalThis;
274
-
275
- if (typeof globalObject?.refreshTranslationsUI === "function") {
276
- globalObject.refreshTranslationsUI();
188
+ if (typeof this.translateActions?.setMessages === "function") {
189
+ this.translateActions.setMessages(this.translates);
277
190
  }
278
191
  }
279
192
 
280
193
  syncCachedMessagesToStore() {
281
- if (typeof this.translateActions?.setMessages !== "function") {
282
- return;
194
+ if (typeof this.translateActions?.setMessages === "function") {
195
+ this.translateActions.setMessages(this.translates);
283
196
  }
284
-
285
- this.translateActions.setMessages({
286
- ...this.translates,
287
- });
288
197
  }
289
198
 
290
199
  getPersistRequestToken(store, type, key) {
291
- return [
292
- this.language,
293
- this.normalizeId(this.defaultCompany?.id),
294
- String(store || "").trim(),
295
- String(type || "").trim(),
296
- String(key || "").trim(),
297
- ].join("::");
298
- }
299
-
300
- resolveReferenceId(value) {
301
- if (!value) return null;
302
-
303
- if (typeof value === "object") {
304
- return this.normalizeId(value.id || value["@id"] || value.people || value.language);
305
- }
306
-
307
- return this.normalizeId(value);
200
+ return `${this.language}:${this.normalizeId(this.defaultCompany?.id)}:${store}:${type}:${key}`;
308
201
  }
309
202
 
310
203
  cacheTranslateRecord(record, fallbackCompanyId = null, fallbackLanguage = this.language) {
311
- const companyId = this.resolveReferenceId(record?.people) || this.normalizeId(fallbackCompanyId);
204
+ const companyId =
205
+ this.resolveReferenceId(record?.people) || this.normalizeId(fallbackCompanyId);
312
206
  const language =
313
- this.normalizeLanguageCode(record?.language?.language || record?.language?.locale || fallbackLanguage) ||
314
- this.language;
207
+ this.normalizeLanguageCode(
208
+ record?.language?.language || record?.language?.locale || fallbackLanguage,
209
+ ) || this.language;
315
210
  const store = String(record?.store || "").trim();
316
211
  const type = String(record?.type || "").trim();
317
212
  const key = String(record?.key || "").trim();
@@ -321,42 +216,47 @@ export default class Translate {
321
216
  return false;
322
217
  }
323
218
 
324
- const previousMessage = this.getStoreBucket(companyId, store, false, language)?.[type]?.[key];
219
+ const previousMessage = this.getStoreBucket(companyId, store, false, language)?.[
220
+ type
221
+ ]?.[key];
325
222
  this.findMessage(store, type, key, message, companyId, language);
326
223
 
327
224
  return previousMessage !== message;
328
225
  }
329
-
330
- getMessageFromBuckets(store, type, key) {
331
- const companyIds = [
332
- this.currentCompany?.id,
333
- this.defaultCompany?.id,
334
- ]
335
- .map((value) => this.normalizeId(value))
336
- .filter(Boolean);
337
-
338
- for (const companyId of companyIds) {
339
- const companyMessage =
340
- this.getStoreBucket(companyId, store)?.[type]?.[key];
341
-
342
- if (companyMessage) {
343
- return companyMessage;
344
- }
345
- }
346
-
347
- return this.getStoreBucket(null, store)?.[type]?.[key];
348
- }
349
-
226
+
227
+ getMessageFromBuckets(store, type, key) {
228
+ const companyIds = [this.currentCompany?.id, this.defaultCompany?.id]
229
+ .map((value) => this.normalizeId(value))
230
+ .filter(Boolean);
231
+
232
+ // Prefer current company, then main/default company, then language-global bucket.
233
+ // Empty/whitespace values do not count as a hit so fallback can still apply.
234
+ for (const companyId of companyIds) {
235
+ const companyMessage = this.getStoreBucket(companyId, store)?.[type]?.[key];
236
+ if (isNonEmptyMessage(companyMessage)) {
237
+ return companyMessage;
238
+ }
239
+ }
240
+
241
+ const globalMessage = this.getStoreBucket(null, store)?.[type]?.[key];
242
+ if (isNonEmptyMessage(globalMessage)) {
243
+ return globalMessage;
244
+ }
245
+
246
+ return globalMessage;
247
+ }
248
+
350
249
  persistMissingTranslate(store, type, key, translate) {
351
250
  if (!store || !type || !key || !this.defaultCompany?.id) return;
352
251
 
353
- // Skip writes when the default company is not available in the current access scope.
354
252
  const defaultCompanyId = this.normalizeId(this.defaultCompany?.id);
355
- if (
356
- !Array.isArray(this.companies) ||
357
- !this.companies.some((company) => this.normalizeId(company?.id) === defaultCompanyId)
358
- )
253
+ // Queueing is a local pending state used to drive resolve/discovery.
254
+ // Do NOT require defaultCompany to appear in this.companies: non-superadmin
255
+ // tenants often lack membership on the main company, but still must be able
256
+ // to resolve and cache the main catalog (read path). Backend gates writes.
257
+ if (!defaultCompanyId) {
359
258
  return;
259
+ }
360
260
 
361
261
  if (this.hasPendingTranslate(store, type, key)) {
362
262
  return;
@@ -397,10 +297,14 @@ export default class Translate {
397
297
  key,
398
298
  translate,
399
299
  revised: false,
400
- })
300
+ }),
401
301
  )
402
302
  .then((result) => {
403
- const changed = this.cacheTranslateRecord(result, defaultCompanyId, this.language);
303
+ const changed = this.cacheTranslateRecord(
304
+ result,
305
+ defaultCompanyId,
306
+ this.language,
307
+ );
404
308
  this.removePendingTranslate(store, type, key);
405
309
  this.persist();
406
310
  if (changed) {
@@ -418,15 +322,13 @@ export default class Translate {
418
322
  }
419
323
 
420
324
  removePendingTranslate(store, type, key) {
421
- if (!store || !type || !key || !this.defaultCompany?.id) return;
422
-
423
325
  if (typeof this.translateActions?.removePendingTranslate !== "function") {
424
326
  return;
425
327
  }
426
328
 
427
329
  this.translateActions.removePendingTranslate({
428
330
  language: this.language,
429
- companyId: this.defaultCompany.id,
331
+ companyId: this.defaultCompany?.id,
430
332
  store,
431
333
  type,
432
334
  key,
@@ -434,44 +336,20 @@ export default class Translate {
434
336
  }
435
337
 
436
338
  getQueuedTranslateGroups() {
437
- const defaultCompanyId = this.normalizeId(this.defaultCompany?.id);
438
- if (!defaultCompanyId) {
439
- return [];
440
- }
441
-
442
- const companyBucket = this.getPendingCompanyBucket(defaultCompanyId, this.language);
443
- if (!companyBucket) {
444
- return [];
445
- }
446
-
447
- const requests = [];
448
- Object.entries(companyBucket).forEach(([store, storeBucket]) => {
449
- if (!storeBucket || typeof storeBucket !== "object") {
450
- return;
451
- }
452
-
453
- Object.entries(storeBucket).forEach(([type, typeBucket]) => {
454
- if (!typeBucket || typeof typeBucket !== "object") {
455
- return;
456
- }
457
-
458
- const keys = Object.keys(typeBucket)
459
- .map((value) => String(value).trim())
460
- .filter(Boolean);
461
-
462
- if (keys.length === 0) {
463
- return;
464
- }
465
-
466
- requests.push({
467
- store,
468
- type,
469
- keys,
470
- });
471
- });
472
- });
339
+ return readQueuedGroups(
340
+ this.translateStore,
341
+ this.defaultCompany?.id,
342
+ this.language,
343
+ );
344
+ }
473
345
 
474
- return requests;
346
+ getQueuedTranslateGroupsForStore(store) {
347
+ return readQueuedGroupsForStore(
348
+ this.translateStore,
349
+ this.defaultCompany?.id,
350
+ store,
351
+ this.language,
352
+ );
475
353
  }
476
354
 
477
355
  scheduleQueuedTranslateResolution() {
@@ -479,7 +357,9 @@ export default class Translate {
479
357
  return this.pendingTranslateResolutionPromise;
480
358
  }
481
359
 
482
- this.pendingTranslateResolutionPromise = Promise.resolve()
360
+ this.pendingTranslateResolutionPromise = new Promise((resolve) => {
361
+ setTimeout(resolve, 0);
362
+ })
483
363
  .then(() => this.resolveQueuedTranslations())
484
364
  .catch(() => this.translates)
485
365
  .finally(() => {
@@ -499,33 +379,44 @@ export default class Translate {
499
379
  return this.translates;
500
380
  }
501
381
 
502
- const currentCompanyId = this.normalizeId(this.currentCompany?.id || this.defaultCompany?.id);
503
- if (!currentCompanyId) {
382
+ // Resolve against every company we keep in cache (current + main) so fallback
383
+ // translations from the main company are available at runtime.
384
+ const companies = this.getCompaniesToCache();
385
+ if (companies.length === 0) {
504
386
  return this.translates;
505
387
  }
506
388
 
507
- const response = await this.translateActions.resolveQueuedMessages({
508
- people: "/people/" + currentCompanyId,
509
- language: this.language,
510
- requests,
511
- });
389
+ let changed = false;
390
+ const allResolvedItems = [];
512
391
 
513
- const resolvedItems = Array.isArray(response)
514
- ? response
515
- : response?.member || response?.["hydra:member"] || [];
392
+ for (const company of companies) {
393
+ const companyId = this.normalizeId(company?.id);
394
+ if (!companyId) continue;
516
395
 
517
- let changed = false;
518
- resolvedItems.forEach((item) => {
519
- changed = this.cacheTranslateRecord(item, currentCompanyId, this.language) || changed;
520
- this.removePendingTranslate(item.store, item.type, item.key);
521
- });
396
+ const response = await this.translateActions.resolveQueuedMessages({
397
+ people: "/people/" + companyId,
398
+ language: this.language,
399
+ requests,
400
+ });
401
+
402
+ const resolvedItems = Array.isArray(response)
403
+ ? response
404
+ : response?.member || response?.["hydra:member"] || [];
405
+
406
+ resolvedItems.forEach((item) => {
407
+ changed =
408
+ this.cacheTranslateRecord(item, companyId, this.language) || changed;
409
+ this.removePendingTranslate(item.store, item.type, item.key);
410
+ allResolvedItems.push(item);
411
+ });
412
+ }
522
413
 
523
414
  if (changed) {
524
415
  this.persist();
525
416
  this.notifyTranslationsUpdated();
526
417
  }
527
418
 
528
- return resolvedItems;
419
+ return allResolvedItems;
529
420
  }
530
421
 
531
422
  t(store, type, key) {
@@ -533,183 +424,76 @@ export default class Translate {
533
424
  const fallbackTranslate = this.formatMessage(key);
534
425
 
535
426
  if (!translate) {
536
- this.persistMissingTranslate(store, type, key, fallbackTranslate);
427
+ const scheduleKey = `${store}:${type}:${key}`;
428
+ if (!this.pendingMissingTranslateSchedules.has(scheduleKey)) {
429
+ this.pendingMissingTranslateSchedules.add(scheduleKey);
430
+ setTimeout(() => {
431
+ this.pendingMissingTranslateSchedules.delete(scheduleKey);
432
+ this.persistMissingTranslate(store, type, key, fallbackTranslate);
433
+ }, 0);
434
+ }
537
435
 
436
+ this.ensureStoreDiscovered(store);
538
437
  return fallbackTranslate;
539
438
  }
540
439
 
541
440
  return translate;
542
- }
543
-
441
+ }
442
+
544
443
  reload() {
545
- this.clear();
546
- return this.discoveryAll({ force: true });
444
+ this.translates = loadStorageObject(TRANSLATES_STORAGE_KEY);
445
+ this.syncCachedMessagesToStore();
547
446
  }
548
447
 
549
448
  clear() {
550
449
  this.translates = {};
551
450
  this.discoveredStores.clear();
552
451
  this.pendingStoreDiscoveries.clear();
553
- this.pendingTranslateResolutionPromise = null;
554
- this.pendingPersistRequests.clear();
555
- if (typeof this.translateActions?.setPendingMessages === "function") {
556
- this.translateActions.setPendingMessages({});
557
- }
558
452
  this.persist();
559
453
  }
560
454
 
561
455
  shouldFetchStoreForCompany(store, company, force = false) {
562
456
  if (force) return true;
563
-
564
- const companyId = this.normalizeId(company?.id);
565
- if (!companyId || !store) return false;
566
-
567
- const storeBucket = this.getStoreBucket(companyId, store, false, this.language);
568
- return storeBucket == null;
457
+ if (!company?.id || !store) return false;
458
+ return this.getStoreBucket(company.id, store) == null;
569
459
  }
570
460
 
571
461
  async discoveryAll(options = {}) {
572
- const companies = this.getCompaniesToCache();
573
-
574
- for (const company of companies) {
575
- for (const store of this.getStoreList()) {
576
- await this.discoveryStoreTranslate(store, {
577
- ...options,
578
- companies: [company],
579
- });
580
- }
581
- }
582
-
583
- return this.translates;
462
+ return runDiscoveryAll(this, options);
584
463
  }
585
464
 
586
465
  async discoveryStoreTranslate(store, options = {}) {
587
- if (!store) return this.translates;
588
-
589
- const force = options.force === true;
590
- if (!force && this.hasDiscoveredStore(store)) {
591
- return this.translates;
592
- }
593
-
594
- const companies = Array.isArray(options.companies) && options.companies.length > 0
595
- ? options.companies
596
- : this.getCompaniesToCache();
597
- if (companies.length === 0) {
598
- this.markStoreDiscovered(store);
599
- return this.translates;
600
- }
601
-
602
- let hasChanges = false;
603
- for (const company of companies) {
604
- const changed = await this.fetchTranslates(store, company, { force });
605
- hasChanges = changed || hasChanges;
606
- }
607
-
608
- if (this.hasCachedBootstrapStore(store)) {
609
- this.markStoreDiscovered(store);
610
- }
611
-
612
- if (hasChanges) {
613
- this.persist();
614
- this.notifyTranslationsUpdated();
615
- }
616
-
617
- return this.translates;
466
+ return runDiscoveryStoreTranslate(this, store, options);
618
467
  }
619
468
 
620
469
  async fetchTranslates(store, company, options = {}) {
621
- if (!company?.id) return false;
622
-
623
- const force = options.force === true;
624
- if (!this.shouldFetchStoreForCompany(store, company, force)) {
625
- return false;
626
- }
627
-
628
- const params = {
629
- store: store,
630
- "language.language": this.language,
631
- people: "/people/" + company.id,
632
- };
633
-
634
- const storeTranslates = [];
635
- let page = 1;
636
- let firstPageSize = null;
637
- const maxPages = 1000;
638
-
639
- while (page <= maxPages) {
640
- const pageItems = await this.translateActions.getItems({
641
- ...params,
642
- page,
643
- });
644
-
645
- if (!Array.isArray(pageItems) || pageItems.length === 0) break;
646
-
647
- storeTranslates.push(...pageItems);
648
- if (firstPageSize == null) firstPageSize = pageItems.length;
649
- if (pageItems.length < firstPageSize) break;
650
-
651
- page += 1;
652
- }
653
-
654
- const companyId = this.normalizeId(company?.id);
655
- const storeBucket = this.getStoreBucket(companyId, store, true);
656
- let changed = false;
657
-
658
- if (storeBucket) {
659
- if (Object.keys(storeBucket).length > 0) {
660
- changed = true;
661
- }
662
- Object.keys(storeBucket).forEach((type) => {
663
- delete storeBucket[type];
664
- });
665
- }
666
-
667
- storeTranslates.forEach((element) => {
668
- changed = this.cacheTranslateRecord(
669
- {
670
- ...element,
671
- people: element.people || companyId,
672
- },
673
- companyId,
674
- this.language,
675
- ) || changed;
676
- this.removePendingTranslate(element.store, element.type, element.key);
677
- });
470
+ return runFetchTranslates(this, store, company, options);
471
+ }
678
472
 
679
- return changed || storeTranslates.length === 0;
473
+ async resolveStoreTranslates(store, company, options = {}) {
474
+ return runResolveStoreTranslates(this, store, company, options);
680
475
  }
681
476
 
682
477
  persist() {
683
478
  if (typeof localStorage !== "undefined") {
684
479
  localStorage.setItem("translates", JSON.stringify(this.translates));
685
480
  }
686
-
687
481
  this.syncCachedMessagesToStore();
688
482
  }
689
483
 
690
484
  findMessage(store, type, key, message, companyId = null, language = this.language) {
691
- const storeBucket = this.getStoreBucket(companyId, store, true, language);
692
- if (!storeBucket) {
485
+ const written = writeMessageToBucket(
486
+ this.translates,
487
+ store,
488
+ type,
489
+ key,
490
+ message,
491
+ companyId,
492
+ language,
493
+ );
494
+ if (written == null) {
693
495
  return this.formatMessage(key);
694
496
  }
695
-
696
- if (!storeBucket[type]) {
697
- storeBucket[type] = {};
698
- }
699
-
700
- if (message !== null)
701
- storeBucket[type][key] = message;
702
-
703
- return storeBucket[type][key] || this.formatMessage(key);
704
- }
705
-
706
- formatMessage(key) {
707
- if (!key) return "";
708
-
709
- return key
710
- .replace(/([a-z])([A-Z])/g, "$1_$2")
711
- .replace(/_/g, " ")
712
- .replace(/-/g, " ")
713
- .replace(/^\w/, (c) => c.toUpperCase());
714
- }
715
- }
497
+ return written || this.formatMessage(key);
498
+ }
499
+ }