@eifi1/ui-kit 0.44.0 → 0.46.0

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 (285) hide show
  1. package/README.md +29 -14
  2. package/dist/admin/admin-action-log-labels.js +9 -0
  3. package/dist/admin/admin-action-log-labels.js.map +1 -1
  4. package/dist/admin/admin-action-log.d.ts +18 -1
  5. package/dist/admin/admin-action-log.js +25 -16
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/admin-area-labels.d.ts +15 -0
  8. package/dist/admin/admin-area-labels.js +6 -1
  9. package/dist/admin/admin-area-labels.js.map +1 -1
  10. package/dist/admin/admin-overview.d.ts +16 -1
  11. package/dist/admin/admin-overview.js +12 -1
  12. package/dist/admin/admin-overview.js.map +1 -1
  13. package/dist/admin/mail-test-card-labels.d.ts +3 -0
  14. package/dist/admin/mail-test-card-labels.js +17 -0
  15. package/dist/admin/mail-test-card-labels.js.map +1 -0
  16. package/dist/admin/mail-test-card.d.ts +75 -0
  17. package/dist/admin/mail-test-card.js +82 -0
  18. package/dist/admin/mail-test-card.js.map +1 -0
  19. package/dist/components/appearance-labels.d.ts +10 -0
  20. package/dist/components/appearance-labels.js +6 -1
  21. package/dist/components/appearance-labels.js.map +1 -1
  22. package/dist/components/data-table-column-settings.js +2 -2
  23. package/dist/components/data-table-column-settings.js.map +1 -1
  24. package/dist/components/data-table-head.js +2 -2
  25. package/dist/components/data-table-head.js.map +1 -1
  26. package/dist/components/data-table-mobile.js +17 -2
  27. package/dist/components/data-table-mobile.js.map +1 -1
  28. package/dist/components/data-table-row-actions.d.ts +18 -2
  29. package/dist/components/data-table-row-actions.js +86 -2
  30. package/dist/components/data-table-row-actions.js.map +1 -1
  31. package/dist/components/data-table-shared.d.ts +2 -0
  32. package/dist/components/data-table-shared.js +17 -2
  33. package/dist/components/data-table-shared.js.map +1 -1
  34. package/dist/components/data-table.d.ts +43 -2
  35. package/dist/components/data-table.js +10 -10
  36. package/dist/components/data-table.js.map +1 -1
  37. package/dist/components/date-format-setting.d.ts +29 -0
  38. package/dist/components/date-format-setting.js +36 -0
  39. package/dist/components/date-format-setting.js.map +1 -0
  40. package/dist/components/swipeable-row.d.ts +8 -0
  41. package/dist/components/swipeable-row.js.map +1 -1
  42. package/dist/components/tabs.d.ts +3 -2
  43. package/dist/components/tabs.js +1 -1
  44. package/dist/components/tabs.js.map +1 -1
  45. package/dist/components/translation-review-editor.js +1 -0
  46. package/dist/components/translation-review-editor.js.map +1 -1
  47. package/dist/components/translation-review-labels.d.ts +5 -0
  48. package/dist/components/translation-review-labels.js +2 -0
  49. package/dist/components/translation-review-labels.js.map +1 -1
  50. package/dist/components/translation-review.d.ts +18 -8
  51. package/dist/components/translation-review.js +64 -7
  52. package/dist/components/translation-review.js.map +1 -1
  53. package/dist/components/write-lock-labels.js +2 -1
  54. package/dist/components/write-lock-labels.js.map +1 -1
  55. package/dist/components/write-lock.d.ts +33 -3
  56. package/dist/components/write-lock.js +9 -0
  57. package/dist/components/write-lock.js.map +1 -1
  58. package/dist/data-table.d.ts +1 -1
  59. package/dist/data-table.js.map +1 -1
  60. package/dist/eslint/index.js +1 -1
  61. package/dist/feedback/feedback-row-detail.d.ts +4 -2
  62. package/dist/feedback/feedback-row-detail.js +17 -26
  63. package/dist/feedback/feedback-row-detail.js.map +1 -1
  64. package/dist/feedback/feedback-swipe.d.ts +18 -0
  65. package/dist/feedback/feedback-swipe.js +10 -1
  66. package/dist/feedback/feedback-swipe.js.map +1 -1
  67. package/dist/i18n/defaults.js +9 -1
  68. package/dist/i18n/defaults.js.map +1 -1
  69. package/dist/i18n/german.js +81 -5
  70. package/dist/i18n/german.js.map +1 -1
  71. package/dist/i18n/kit-labels.d.ts +25 -1
  72. package/dist/i18n/kit-labels.js +8 -2
  73. package/dist/i18n/kit-labels.js.map +1 -1
  74. package/dist/i18n/locales/en.js +97 -7
  75. package/dist/i18n/locales/en.js.map +3 -3
  76. package/dist/i18n/locales/es.js +81 -5
  77. package/dist/i18n/locales/es.js.map +1 -1
  78. package/dist/i18n/locales/fr.js +81 -5
  79. package/dist/i18n/locales/fr.js.map +1 -1
  80. package/dist/i18n/locales/hu.js +81 -5
  81. package/dist/i18n/locales/hu.js.map +1 -1
  82. package/dist/i18n/locales/it.js +81 -5
  83. package/dist/i18n/locales/it.js.map +1 -1
  84. package/dist/i18n/locales/zh.js +81 -5
  85. package/dist/i18n/locales/zh.js.map +1 -1
  86. package/dist/i18n/review.js +16 -2
  87. package/dist/i18n/review.js.map +1 -1
  88. package/dist/index.d.ts +17 -2
  89. package/dist/index.js +40 -0
  90. package/dist/index.js.map +1 -1
  91. package/dist/lib/date-format.d.ts +81 -0
  92. package/dist/lib/date-format.js +56 -0
  93. package/dist/lib/date-format.js.map +1 -0
  94. package/dist/lib/format.d.ts +5 -1
  95. package/dist/lib/format.js +11 -5
  96. package/dist/lib/format.js.map +1 -1
  97. package/dist/notifications/device-zone-report.d.ts +27 -0
  98. package/dist/notifications/device-zone-report.js +24 -0
  99. package/dist/notifications/device-zone-report.js.map +1 -0
  100. package/dist/notifications/feedback-notification-types-labels.d.ts +7 -1
  101. package/dist/notifications/feedback-notification-types-labels.js +4 -1
  102. package/dist/notifications/feedback-notification-types-labels.js.map +1 -1
  103. package/dist/notifications/feedback-notification-types.d.ts +11 -5
  104. package/dist/notifications/feedback-notification-types.js +2 -1
  105. package/dist/notifications/feedback-notification-types.js.map +1 -1
  106. package/dist/notifications/notification-bell.d.ts +33 -0
  107. package/dist/notifications/notification-bell.js +74 -0
  108. package/dist/notifications/notification-bell.js.map +1 -0
  109. package/dist/notifications/notification-inbox.d.ts +77 -0
  110. package/dist/notifications/notification-inbox.js +206 -0
  111. package/dist/notifications/notification-inbox.js.map +1 -0
  112. package/dist/notifications/notification-types-setting.d.ts +6 -4
  113. package/dist/notifications/notification-types-setting.js +103 -45
  114. package/dist/notifications/notification-types-setting.js.map +1 -1
  115. package/dist/notifications/notification-types.d.ts +19 -7
  116. package/dist/notifications/notifications-labels.d.ts +41 -7
  117. package/dist/notifications/notifications-labels.js +18 -1
  118. package/dist/notifications/notifications-labels.js.map +1 -1
  119. package/dist/notifications/push-devices-setting.d.ts +20 -3
  120. package/dist/notifications/push-devices-setting.js +93 -33
  121. package/dist/notifications/push-devices-setting.js.map +1 -1
  122. package/dist/notifications/push-worker.d.ts +108 -0
  123. package/dist/notifications/push-worker.js +62 -0
  124. package/dist/notifications/push-worker.js.map +1 -0
  125. package/dist/notifications/quiet-hours-setting.d.ts +29 -17
  126. package/dist/notifications/quiet-hours-setting.js +9 -41
  127. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  128. package/dist/notifications/time-zone-select.d.ts +5 -2
  129. package/dist/notifications/time-zone-select.js.map +1 -1
  130. package/dist/notifications/use-push-device.d.ts +4 -17
  131. package/dist/notifications/use-push-device.js +7 -15
  132. package/dist/notifications/use-push-device.js.map +1 -1
  133. package/dist/offline/local-first.d.ts +33 -0
  134. package/dist/offline/local-first.js +39 -0
  135. package/dist/offline/local-first.js.map +1 -0
  136. package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
  137. package/dist/offline/pwa-update-prompt-labels.js +9 -0
  138. package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
  139. package/dist/offline/pwa-update-prompt.d.ts +32 -0
  140. package/dist/offline/pwa-update-prompt.js +36 -0
  141. package/dist/offline/pwa-update-prompt.js.map +1 -0
  142. package/dist/offline/query-persistence.d.ts +125 -0
  143. package/dist/offline/query-persistence.js +183 -0
  144. package/dist/offline/query-persistence.js.map +1 -0
  145. package/dist/offline/service-worker-updates.d.ts +46 -0
  146. package/dist/offline/service-worker-updates.js +39 -0
  147. package/dist/offline/service-worker-updates.js.map +1 -0
  148. package/dist/offline/shared-computer-labels.d.ts +2 -0
  149. package/dist/offline/shared-computer-labels.js +9 -0
  150. package/dist/offline/shared-computer-labels.js.map +1 -0
  151. package/dist/offline/shared-computer.d.ts +44 -0
  152. package/dist/offline/shared-computer.js +51 -0
  153. package/dist/offline/shared-computer.js.map +1 -0
  154. package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
  155. package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
  156. package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
  157. package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
  158. package/dist/offline/sign-out-pending-dialog.js +66 -0
  159. package/dist/offline/sign-out-pending-dialog.js.map +1 -0
  160. package/dist/offline.d.ts +11 -0
  161. package/dist/offline.js +33 -0
  162. package/dist/offline.js.map +1 -0
  163. package/dist/push-worker.d.ts +2 -0
  164. package/dist/push-worker.js +14 -0
  165. package/dist/push-worker.js.map +1 -0
  166. package/dist/shell/account-menu-text-size.d.ts +33 -0
  167. package/dist/shell/account-menu-text-size.js +51 -0
  168. package/dist/shell/account-menu-text-size.js.map +1 -0
  169. package/dist/shell/admin-menu.d.ts +24 -0
  170. package/dist/shell/admin-menu.js +12 -3
  171. package/dist/shell/admin-menu.js.map +1 -1
  172. package/dist/shell/top-bar-actions.d.ts +6 -2
  173. package/dist/shell/top-bar-actions.js +1 -0
  174. package/dist/shell/top-bar-actions.js.map +1 -1
  175. package/dist/shell.d.ts +4 -0
  176. package/dist/shell.js +4 -0
  177. package/dist/shell.js.map +1 -1
  178. package/dist/sync/db.d.ts +76 -0
  179. package/dist/sync/db.js +24 -0
  180. package/dist/sync/db.js.map +1 -0
  181. package/dist/sync/engine.d.ts +124 -0
  182. package/dist/sync/engine.js +474 -0
  183. package/dist/sync/engine.js.map +1 -0
  184. package/dist/sync/hooks.d.ts +30 -0
  185. package/dist/sync/hooks.js +67 -0
  186. package/dist/sync/hooks.js.map +1 -0
  187. package/dist/sync/testing.d.ts +28 -0
  188. package/dist/sync/testing.js +41 -0
  189. package/dist/sync/testing.js.map +1 -0
  190. package/dist/sync/wire.d.ts +50 -0
  191. package/dist/sync/wire.js +2 -0
  192. package/dist/sync/wire.js.map +1 -0
  193. package/dist/sync.d.ts +7 -0
  194. package/dist/sync.js +22 -0
  195. package/dist/sync.js.map +1 -0
  196. package/dist/tour/tour-catalogue.d.ts +7 -1
  197. package/dist/tour/tour-catalogue.js.map +1 -1
  198. package/dist/tour/tour-labels.js +1 -0
  199. package/dist/tour/tour-labels.js.map +1 -1
  200. package/dist/tour/tour.d.ts +30 -0
  201. package/dist/tour/tour.js +54 -8
  202. package/dist/tour/tour.js.map +1 -1
  203. package/dist/tour/use-tour-progress.d.ts +99 -1
  204. package/dist/tour/use-tour-progress.js +228 -12
  205. package/dist/tour/use-tour-progress.js.map +1 -1
  206. package/dist/tour.d.ts +2 -0
  207. package/dist/tour.js +2 -0
  208. package/dist/tour.js.map +1 -1
  209. package/package.json +43 -1
  210. package/src/admin/admin-action-log-labels.ts +9 -0
  211. package/src/admin/admin-action-log.tsx +41 -5
  212. package/src/admin/admin-area-labels.ts +20 -0
  213. package/src/admin/admin-overview.tsx +37 -2
  214. package/src/admin/mail-test-card-labels.ts +19 -0
  215. package/src/admin/mail-test-card.tsx +190 -0
  216. package/src/components/appearance-labels.ts +16 -0
  217. package/src/components/data-table-column-settings.tsx +2 -2
  218. package/src/components/data-table-head.tsx +2 -2
  219. package/src/components/data-table-mobile.tsx +26 -3
  220. package/src/components/data-table-row-actions.tsx +118 -2
  221. package/src/components/data-table-shared.tsx +39 -3
  222. package/src/components/data-table.tsx +52 -21
  223. package/src/components/date-format-setting.tsx +64 -0
  224. package/src/components/swipeable-row.tsx +8 -0
  225. package/src/components/tabs.tsx +4 -3
  226. package/src/components/translation-review-editor.tsx +2 -0
  227. package/src/components/translation-review-labels.ts +7 -0
  228. package/src/components/translation-review.tsx +102 -14
  229. package/src/components/write-lock-labels.ts +1 -0
  230. package/src/components/write-lock.tsx +36 -3
  231. package/src/data-table.ts +1 -0
  232. package/src/feedback/feedback-row-detail.tsx +18 -23
  233. package/src/feedback/feedback-swipe.tsx +28 -1
  234. package/src/i18n/defaults.ts +8 -0
  235. package/src/i18n/german.ts +90 -4
  236. package/src/i18n/kit-labels.tsx +31 -1
  237. package/src/i18n/locales/en.ts +13 -0
  238. package/src/i18n/locales/es.ts +90 -4
  239. package/src/i18n/locales/fr.ts +90 -4
  240. package/src/i18n/locales/hu.ts +86 -4
  241. package/src/i18n/locales/it.ts +90 -4
  242. package/src/i18n/locales/zh.ts +84 -2
  243. package/src/i18n/review.ts +15 -1
  244. package/src/index.ts +45 -0
  245. package/src/lib/date-format.ts +151 -0
  246. package/src/lib/format.ts +23 -6
  247. package/src/notifications/device-zone-report.ts +54 -0
  248. package/src/notifications/feedback-notification-types-labels.ts +10 -1
  249. package/src/notifications/feedback-notification-types.ts +12 -5
  250. package/src/notifications/notification-bell.tsx +116 -0
  251. package/src/notifications/notification-inbox.tsx +304 -0
  252. package/src/notifications/notification-types-setting.tsx +151 -55
  253. package/src/notifications/notification-types.ts +20 -8
  254. package/src/notifications/notifications-labels.ts +69 -11
  255. package/src/notifications/push-devices-setting.tsx +123 -34
  256. package/src/notifications/push-worker.ts +160 -0
  257. package/src/notifications/quiet-hours-setting.tsx +41 -58
  258. package/src/notifications/time-zone-select.tsx +5 -2
  259. package/src/notifications/use-push-device.ts +20 -33
  260. package/src/offline/local-first.ts +76 -0
  261. package/src/offline/pwa-update-prompt-labels.ts +9 -0
  262. package/src/offline/pwa-update-prompt.tsx +72 -0
  263. package/src/offline/query-persistence.ts +356 -0
  264. package/src/offline/service-worker-updates.ts +81 -0
  265. package/src/offline/shared-computer-labels.ts +8 -0
  266. package/src/offline/shared-computer.tsx +87 -0
  267. package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
  268. package/src/offline/sign-out-pending-dialog.tsx +135 -0
  269. package/src/offline.ts +32 -0
  270. package/src/push-worker.ts +25 -0
  271. package/src/shell/account-menu-text-size.tsx +77 -0
  272. package/src/shell/admin-menu.tsx +40 -3
  273. package/src/shell/top-bar-actions.tsx +6 -1
  274. package/src/shell.ts +7 -0
  275. package/src/sync/db.ts +103 -0
  276. package/src/sync/engine.ts +699 -0
  277. package/src/sync/hooks.ts +93 -0
  278. package/src/sync/testing.ts +73 -0
  279. package/src/sync/wire.ts +61 -0
  280. package/src/sync.ts +29 -0
  281. package/src/tour/tour-catalogue.tsx +7 -1
  282. package/src/tour/tour-labels.ts +1 -0
  283. package/src/tour/tour.tsx +105 -9
  284. package/src/tour/use-tour-progress.ts +378 -12
  285. package/src/tour.ts +3 -0
@@ -134,6 +134,9 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
134
134
  answeredDescription: "当您提交或评论过的反馈收到回复时。",
135
135
  comment: "您关注的反馈中的评论",
136
136
  commentDescription: "当有人评论您提交或评论过的反馈时。",
137
+ received: "新反馈和返工",
138
+ receivedDescription: "当有人提交反馈或退回反馈时。",
139
+ adminGroup: "管理员",
137
140
  },
138
141
  feedbackContext: {
139
142
  user: "用户",
@@ -590,6 +593,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
590
593
  skip: "跳过",
591
594
  done: "完成",
592
595
  awaitClickHint: "点击高亮的元素以继续",
596
+ awaitInputHint: "在高亮的字段中输入一个值以继续", // 0.46: TourStep.awaitInput (keksdose live #436).
593
597
  step: (current, total) => `${n(current)} / ${n(total)}`,
594
598
  },
595
599
  // 0.43.0 — docs/tours-harmonization.md §3, §8: the tours page.
@@ -797,6 +801,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
797
801
  },
798
802
  writeLock: {
799
803
  reason: "仅可查看,无法修改。",
804
+ offline: "需要网络连接。",
800
805
  },
801
806
  accountState: {
802
807
  active: "已启用",
@@ -1125,6 +1130,9 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1125
1130
  typesTitle: "通知我以下内容",
1126
1131
  typesDescription: "选择要接收哪些通知,以及接收方式。",
1127
1132
  byEmail: "通过电子邮件",
1133
+ channelApp: "应用内",
1134
+ channelPush: "推送",
1135
+ channelEmail: "电子邮件",
1128
1136
  emailUnverified: "请先确认您的邮箱地址,才能收到这些邮件。",
1129
1137
  digestCadence: "频率",
1130
1138
  digestWeekly: "每周",
@@ -1132,8 +1140,15 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1132
1140
  typesLoading: "正在加载通知设置…",
1133
1141
  typesEmpty: "此账户没有可用的通知。",
1134
1142
  quietTitle: "免打扰时段",
1135
- quietDescription: (inbox) =>
1136
- inbox ? "此时段内不发送推送通知。通知仍会进入您的收件箱。" : "此时段内不发送推送通知。",
1143
+ quietDescription: (inbox, held) =>
1144
+ held
1145
+ ? inbox
1146
+ ? "此时段内推送通知会等待,并在时段结束时送达。通知会立即进入您的收件箱。"
1147
+ : "此时段内推送通知会等待,并在时段结束时送达。"
1148
+ : inbox
1149
+ ? "此时段内不发送推送通知。通知仍会进入您的收件箱。"
1150
+ : "此时段内不发送推送通知。",
1151
+ quietZone: (zone) => `按您的时区(${zone}),随您最近使用的设备而定。`,
1137
1152
  quietFrom: "从",
1138
1153
  quietUntil: "到",
1139
1154
  quietIncomplete: "请同时设置开始和结束时间。两者都设置后,免打扰时段才会生效。",
@@ -1188,6 +1203,19 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1188
1203
  // Starts with `remove`, the button's visible text, for voice control.
1189
1204
  removeItem: (device) => `移除:${device}`,
1190
1205
  removePrompt: "停止向此设备发送通知?",
1206
+ deviceTypes: "此设备上的类型",
1207
+ deviceTypesDescription: "关闭某个类型,即可仅在此设备上将其静音。",
1208
+ deviceTypesEmpty: "没有开启推送通知的类型。",
1209
+ inboxTitle: "收件箱",
1210
+ inboxLoading: "正在加载通知…",
1211
+ inboxEmpty: "没有新通知。",
1212
+ markAllRead: "全部标为已读",
1213
+ clearAll: "全部清除",
1214
+ clearPrompt: "删除收件箱中的所有通知?",
1215
+ showMore: "显示更多",
1216
+ bellLabel: "通知",
1217
+ bellUnread: (count) => `${n(count)} 条未读`,
1218
+ allNotifications: "所有通知",
1191
1219
  unsubscribeTitle: "电子邮件通知",
1192
1220
  unsubscribeAbout: (type) => `您会收到有关以下内容的邮件:${type}。`,
1193
1221
  unsubscribeGeneric: "您会收到通知邮件。",
@@ -1321,11 +1349,14 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1321
1349
  },
1322
1350
  // 0.34 (§11.5): whole phrases per noun. `team` is Kurvenschmiede's "团队"; `company`
1323
1351
  // keeps `actions.membership_remove`'s string.
1352
+ renamedFromTo: (from, to) => `从 ${from} 改为 ${to}`,
1324
1353
  groupActions: {
1325
1354
  group: {
1326
1355
  group_create: "群组已创建",
1327
1356
  group_rename: "群组已重命名",
1328
1357
  group_delete: "群组已删除",
1358
+ group_deactivate: "群组已停用",
1359
+ group_reactivate: "群组已重新激活",
1329
1360
  membership_add: "已添加到群组",
1330
1361
  membership_role: "群组中的角色已更改",
1331
1362
  membership_remove: "已从群组移除",
@@ -1335,6 +1366,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1335
1366
  group_create: "团队已创建",
1336
1367
  group_rename: "团队已重命名",
1337
1368
  group_delete: "团队已删除",
1369
+ group_deactivate: "团队已停用",
1370
+ group_reactivate: "团队已重新激活",
1338
1371
  membership_add: "已添加到团队",
1339
1372
  membership_role: "团队中的角色已更改",
1340
1373
  membership_remove: "已从团队移除",
@@ -1344,6 +1377,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1344
1377
  group_create: "公司已创建",
1345
1378
  group_rename: "公司已重命名",
1346
1379
  group_delete: "公司已删除",
1380
+ group_deactivate: "公司已停用",
1381
+ group_reactivate: "公司已重新激活",
1347
1382
  membership_add: "已添加到公司",
1348
1383
  membership_role: "公司中的角色已更改",
1349
1384
  membership_remove: "已从公司移除",
@@ -1353,6 +1388,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1353
1388
  group_create: "预算已创建",
1354
1389
  group_rename: "预算已重命名",
1355
1390
  group_delete: "预算已删除",
1391
+ group_deactivate: "预算已停用",
1392
+ group_reactivate: "预算已重新激活",
1356
1393
  membership_add: "已添加到预算",
1357
1394
  membership_role: "预算中的角色已更改",
1358
1395
  membership_remove: "已从预算移除",
@@ -1487,6 +1524,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1487
1524
  approve: "通过",
1488
1525
  flag: "需要修改",
1489
1526
  suggest: "建议译文",
1527
+ flagRow: "标记为需修改…",
1528
+ suggestRow: "添加译文…",
1490
1529
  reset: "标记为未审校",
1491
1530
  cancel: "取消",
1492
1531
  approveSelected: "通过所选项",
@@ -1731,6 +1770,14 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1731
1770
  contrastHelp: "增强对比度会加深浅色文字和线条,并加粗焦点框。“跟随系统”沿用此设备的设置。",
1732
1771
  contrastModes: { system: "跟随系统", standard: "标准", more: "增强" },
1733
1772
  saveFailed: "无法保存到您的账户。此设备上仍然生效。",
1773
+ // 0.46: DateFormatSetting (keksdose's words where it has them).
1774
+ dateFormat: "日期格式",
1775
+ dateFormatHelp:
1776
+ "整个应用中日期的书写方式。与界面语言无关,因此您可以用英文阅读应用,同时仍看到德式日期。",
1777
+ dateFormatAuto: "跟随语言",
1778
+ // 0.46: AccountMenuTextSize (keksdose live #426).
1779
+ textSmaller: "缩小文字",
1780
+ textLarger: "放大文字",
1734
1781
  },
1735
1782
  // 0.36.0 — docs/settings-harmonization.md §11.3: resetting this device's settings, the
1736
1783
  // last card of "外观".
@@ -1754,9 +1801,21 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1754
1801
  moreTitle: "更多页面",
1755
1802
  },
1756
1803
  // 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words.
1804
+ mailTestCard: {
1805
+ title: "测试邮件",
1806
+ description: "通过本安装发送所有邮件所用的传输方式,向您自己的地址发送一封邮件。",
1807
+ transport: (backend) => `传输方式:${backend}`,
1808
+ action: "给我发送测试邮件",
1809
+ sent: (address) => `已发送至 ${address}。请查看收件箱。`,
1810
+ console: "控制台传输只记录邮件,未发送任何邮件。",
1811
+ failed: "无法发送。服务器日志中有服务商的回复。",
1812
+ rateLimited,
1813
+ keywords: "测试邮件 邮件 电子邮件 传输 smtp resend 控制台 发送 投递",
1814
+ },
1757
1815
  adminArea: {
1758
1816
  title: "管理",
1759
1817
  waiting: (count) => `${n(count)} 项待处理`,
1818
+ waitingLabel: (label, count) => `${label},${n(count)} 项待处理`,
1760
1819
  forbiddenTitle: "仅限管理员",
1761
1820
  forbiddenBody: (appName) => `管理员在这里管理 ${appName}。您的账户无法打开此页面。`,
1762
1821
  forbiddenHome: "前往首页",
@@ -1774,6 +1833,10 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1774
1833
  invitationsPending: "待接受的邀请",
1775
1834
  users: "用户",
1776
1835
  usersNew: (count) => `近 7 天新增 ${n(count)} 人`,
1836
+ awaitingVerdict: "待确认",
1837
+ awaitingVerdictHint: "已答复,等待您或报告人确认已解决",
1838
+ releasedForWork: "待处理",
1839
+ releasedForWorkHint: "已就绪、已要求返工或进行中",
1777
1840
  },
1778
1841
  overviewError: "无法加载概览。",
1779
1842
  retry: "重试",
@@ -1822,6 +1885,25 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1822
1885
  billingOff: "本地已关闭计费:切换套餐不会改变任何内容",
1823
1886
  },
1824
1887
  },
1888
+ // 0.45.0 — docs/local-first-harmonization.md §1.6 (`@eifi1/ui-kit/offline`): keksdose's
1889
+ // reviewed words (nav.logout_queued_*, pwa.*) where it has the language.
1890
+ signOutPending: {
1891
+ title: "部分修改尚未保存到服务器",
1892
+ lost: (count) => `${n(count)} 项修改仍在等待同步。退出登录会清除此设备上的副本,而这是唯一的副本。`,
1893
+ kept: (count) => `${n(count)} 项修改仍在等待同步。它们会为您保留在此设备上,并在您下次在此登录时同步。`,
1894
+ syncNow: "立即同步",
1895
+ signOutAnyway: "仍然退出登录",
1896
+ discardWaiting: "放弃等待中的修改",
1897
+ cancel: "取消",
1898
+ },
1899
+ pwaUpdate: {
1900
+ updateAvailable: "有新版本可用。",
1901
+ reload: "重新加载",
1902
+ },
1903
+ sharedComputer: {
1904
+ label: "这是一台共用电脑",
1905
+ hint: "此设备上不会保留您的数据副本。页面将只能在联网时打开。",
1906
+ },
1825
1907
  };
1826
1908
  }
1827
1909
 
@@ -330,7 +330,9 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
330
330
  "sessions.lastActive": [["{{when}}"]],
331
331
  "sessions.ip": [["{{address}}"]],
332
332
  "sessions.revokeItem": [["{{device}}"]],
333
- "notifications.quietDescription": [[false], [true]],
333
+ "notifications.quietDescription": [[false], [true], [false, true], [true, true]],
334
+ "notifications.quietZone": [["{{zone}}"]],
335
+ "notifications.bellUnread": one,
334
336
  "notifications.deviceZone": [["{{zone}}"]],
335
337
  "notifications.iosInstall": [["{{app}}"]],
336
338
  "notifications.testSent": one,
@@ -353,6 +355,8 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
353
355
  "adminActionLog.by": [["{{actor}}"]],
354
356
  "adminActionLog.filteredTo": [["{{target}}"]],
355
357
  "adminActionLog.filterBy": [["{{target}}"]],
358
+ // 0.45.0 (admin §9.4): a company rename that logged both names.
359
+ "adminActionLog.renamedFromTo": [["{{from}}", "{{to}}"]],
356
360
  "transferOwnership.title": [["{{name}}"]],
357
361
  "transferOwnership.unavailable": [["{{account}}", "{{reason}}"]],
358
362
  "invitations.sent": [["{{date}}"]],
@@ -406,6 +410,12 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
406
410
  // 0.42.0 (docs/admin-harmonization.md §3.1): the menu row's count, the 403 page's app,
407
411
  // the users tile's new accounts.
408
412
  "adminArea.waiting": one,
413
+ // 0.45.0 (admin §9.3.4): an IconButton's name with its waiting count.
414
+ "adminArea.waitingLabel": [["{{label}}", 1], ["{{label}}", 3]],
415
+ // 0.45.0 (admin §9.2.4): the test mail's transport, masked address and limiter.
416
+ "mailTestCard.transport": [["{{backend}}"]],
417
+ "mailTestCard.sent": [["{{address}}"]],
418
+ "mailTestCard.rateLimited": [[], [30], [300]],
409
419
  "adminArea.forbiddenBody": [["{{app}}"]],
410
420
  "adminArea.tiles.usersNew": one,
411
421
  // 0.42.0 (docs/top-bar-harmonization.md §3.1): the sync indicator's counts, its time, the
@@ -431,4 +441,8 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
431
441
  "tourCatalogue.steps": one,
432
442
  "tourCatalogue.duration": one,
433
443
  "tourCatalogue.completedOn": [["{{date}}"]],
444
+ // 0.45.0 (docs/local-first-harmonization.md §1.6): the sign-out question's count, in
445
+ // both word sets.
446
+ "signOutPending.lost": one,
447
+ "signOutPending.kept": one,
434
448
  };
package/src/index.ts CHANGED
@@ -408,6 +408,8 @@ export * from "./components/form-actions";
408
408
  // (docs/billing-harmonization.md §12.36): the kinds, the holds, `CommitScope`,
409
409
  // `COMMIT_EXCEPT_BILLING` and the pure `writeLockFor`.
410
410
  export { WriteLockProvider, useWriteLock, writeLockFor, COMMIT_EXCEPT_BILLING, DEFAULT_WRITE_LOCK_LABELS } from "./components/write-lock";
411
+ // 0.46 (kastlan K4): the offline lock kind, its scope and its source.
412
+ export { OFFLINE_LOCK, COMMIT_EXCEPT_OFFLINE, useOfflineWriteLock } from "./components/write-lock";
411
413
  export type { WriteLock, WriteLockLabels, WriteLockProviderProps, WriteLockKind, WriteLockHold, CommitScope } from "./components/write-lock";
412
414
  // 0.18: the parts both apps' admin rosters repeat (Kurvenschmiede 6).
413
415
  export { RoleChip, AccountStateChip, DateMark, dateColumn, DEFAULT_ACCOUNT_STATE_LABELS, ACCOUNT_STATE_TONES } from "./components/account-chips";
@@ -552,6 +554,19 @@ export type {
552
554
  // actions, one of the behaviours at Large (§4, §10.7–10.9).
553
555
  export { DEFAULT_APPEARANCE_LABELS, useAppearanceLabels } from "./components/appearance-labels";
554
556
  export type { AppearanceLabels } from "./components/appearance-labels";
557
+ // 0.46 (keksdose's date format, lifted): the preference, its resolver and renderer, the
558
+ // provider's formatter for it, and the settings field.
559
+ export {
560
+ DATE_FORMAT_PREFERENCES,
561
+ isDateFormatPreference,
562
+ resolveDateFormat,
563
+ formatDateWithPreference,
564
+ dateFormatSample,
565
+ dateFormatterFor,
566
+ } from "./lib/date-format";
567
+ export type { DateFormatPreference, DateFormatOptions, ResolvedDateFormat } from "./lib/date-format";
568
+ export { DateFormatSetting } from "./components/date-format-setting";
569
+ export type { DateFormatSettingProps } from "./components/date-format-setting";
555
570
  export { RowActions, rowActionsColumn, DEFAULT_ROW_ACTIONS_LABELS } from "./components/row-actions";
556
571
  export type {
557
572
  RowAction,
@@ -808,9 +823,16 @@ export type {
808
823
  AdminExtraTile,
809
824
  AdminOverviewProps,
810
825
  AttentionTileProps,
826
+ // 0.46 (Kurvenschmiede 1): the feedback tiles with the kit's words.
827
+ AdminFeedbackTile,
828
+ AdminFeedbackTileKey,
829
+ AdminOwnTile,
811
830
  } from "./admin/admin-overview";
812
831
  export { AdminToolLinks } from "./admin/admin-tool-links";
813
832
  export type { AdminToolLink, AdminToolLinksProps } from "./admin/admin-tool-links";
833
+ // 0.45.0 (docs/admin-harmonization.md §9.2.4): "Send a test mail to me".
834
+ export { MailTestCard, DEFAULT_MAIL_TEST_CARD_LABELS, MAIL_TEST_ANCHOR, maskMailAddress } from "./admin/mail-test-card";
835
+ export type { MailTestCardProps, MailTestCardLabels, MailTestAnswer } from "./admin/mail-test-card";
814
836
  // 0.33.0: the operator's plan parts (docs/billing-harmonization.md §14.12).
815
837
  export {
816
838
  planColumn,
@@ -943,6 +965,7 @@ export type {
943
965
  NotificationTypeGroup,
944
966
  NotificationSettingsPatch,
945
967
  PushDeviceItem,
968
+ NotificationChannel,
946
969
  } from "./notifications/notification-types";
947
970
  export { NotificationTypesSetting } from "./notifications/notification-types-setting";
948
971
  export type { NotificationTypesSettingProps } from "./notifications/notification-types-setting";
@@ -950,6 +973,14 @@ export { QuietHoursSetting } from "./notifications/quiet-hours-setting";
950
973
  export type { QuietHoursSettingProps } from "./notifications/quiet-hours-setting";
951
974
  export { TimeZoneSelect, timeZoneOptions, deviceTimeZone } from "./notifications/time-zone-select";
952
975
  export type { TimeZoneSelectProps } from "./notifications/time-zone-select";
976
+ // 0.46 (docs/notifications-harmonization.md §10.1, §10.3): the inbox list, the bell and
977
+ // the device-zone report (the account's zone is automatic only, amended 2026-10-20).
978
+ export { NotificationInbox, INBOX_BADGE_INTERVAL } from "./notifications/notification-inbox";
979
+ export type { NotificationInboxItem, NotificationInboxProps } from "./notifications/notification-inbox";
980
+ export { NotificationBell } from "./notifications/notification-bell";
981
+ export type { NotificationBellProps } from "./notifications/notification-bell";
982
+ export { useDeviceZoneReport } from "./notifications/device-zone-report";
983
+ export type { DeviceZoneReportOptions } from "./notifications/device-zone-report";
953
984
  export {
954
985
  usePushDevice,
955
986
  PUSH_SUBSCRIPTION_CHANGED,
@@ -964,6 +995,20 @@ export type {
964
995
  PushPermission,
965
996
  PushDeviceErrorCode,
966
997
  } from "./notifications/use-push-device";
998
+ // 0.46 (NT:1028): the service worker re-registers a rotated subscription. A worker imports
999
+ // them from `@eifi1/ui-kit/push-worker` (§10.6 rule 5); the barrel keeps them too.
1000
+ export {
1001
+ handlePushSubscriptionChange,
1002
+ resubscribeAfterChange,
1003
+ pushSubscriptionInput,
1004
+ } from "./notifications/push-worker";
1005
+ export type {
1006
+ PushReplaceInput,
1007
+ PushSubscriptionChangeOptions,
1008
+ PushSubscriptionChangeEventLike,
1009
+ PushResubscribeOutcome,
1010
+ PushWorkerScope,
1011
+ } from "./notifications/push-worker";
967
1012
  export { PushDeviceSetting } from "./notifications/push-device-setting";
968
1013
  export type { PushDeviceSettingProps, PushDeviceAction } from "./notifications/push-device-setting";
969
1014
  export { PushDevicesSetting, describeUserAgent } from "./notifications/push-devices-setting";
@@ -0,0 +1,151 @@
1
+ import { formatLocaleOf } from "../i18n/languages";
2
+ import type { KitDateFormatter } from "../i18n/kit-labels";
3
+
4
+ /**
5
+ * The date-format preference (0.46, lifted from keksdose's `date-format-store.ts`,
6
+ * feedback #180): how a DAY is written — the order of day, month and year and the
7
+ * separator between them — as a choice of its own, apart from the interface language.
8
+ * "Read the app in English and still see German dates", or ISO everywhere.
9
+ *
10
+ * keksdose's rules, which the kit keeps:
11
+ *
12
+ * 1. **A label that identifies a day follows the preference** — anything that prints
13
+ * day, month and year goes through {@link formatDateWithPreference} (or the
14
+ * provider's `formatDate`, {@link dateFormatterFor}, for the kit's own date fields).
15
+ * 2. **A label without a day follows the language**: "August 2026", a month or weekday
16
+ * NAME has no order to arrange. {@link dateFormatterFor} answers `""` for a month or a
17
+ * year, so the kit keeps its own rendering in the language.
18
+ * 3. **A clock time follows the language**: `sv-SE` is a statement about digit order in
19
+ * a date, not about a 12- or 24-hour clock.
20
+ * 4. **A weekday's NAME follows the language** (dev#546): `sv-SE` means "year first,
21
+ * with dashes", not "the reader speaks Swedish" — so with `weekday` the day's name is
22
+ * the language's, joined to the date the preference renders.
23
+ *
24
+ * The preference is the APP's to keep — keksdose holds it per device (a persisted store,
25
+ * not an account column: nothing server-written formats a date for the reader). The kit
26
+ * owns the vocabulary, the resolver, the renderer and the settings field
27
+ * ({@link DateFormatSetting}).
28
+ */
29
+
30
+ /** `"auto"` follows the interface language (the default — nobody's dates move until
31
+ * they ask); the rest are BCP 47 tags chosen for the SHAPE they produce: German
32
+ * 01.08.2026, British 01/08/2026, US 08/01/2026, ISO 2026-08-01. */
33
+ export type DateFormatPreference = "auto" | "de-DE" | "en-GB" | "en-US" | "sv-SE";
34
+
35
+ /** The options in the order the setting lists them: `auto` first (the default), then
36
+ * German → UK → US → ISO. */
37
+ export const DATE_FORMAT_PREFERENCES: readonly DateFormatPreference[] = ["auto", "de-DE", "en-GB", "en-US", "sv-SE"];
38
+
39
+ /** Whether `value` is one of {@link DATE_FORMAT_PREFERENCES} — for a value read back from
40
+ * storage, where anything else falls back to `"auto"`. */
41
+ export function isDateFormatPreference(value: unknown): value is DateFormatPreference {
42
+ return typeof value === "string" && (DATE_FORMAT_PREFERENCES as readonly string[]).includes(value);
43
+ }
44
+
45
+ /** A resolved preference: the locale and `Intl` options the date itself is rendered
46
+ * with, and — only when asked for — the locale the weekday's NAME comes from. */
47
+ export interface ResolvedDateFormat {
48
+ locale: string | undefined;
49
+ options: Intl.DateTimeFormatOptions;
50
+ /** The language's format locale, present when `weekday` was asked for. */
51
+ weekday?: { locale: string | undefined };
52
+ }
53
+
54
+ /** What {@link resolveDateFormat} and the renderers take beyond the preference. */
55
+ export interface DateFormatOptions {
56
+ /** Name the day of the week too, in the interface language ("Mo., 01.08.2026"). A date
57
+ * in a COLUMN carries it; a date in a SENTENCE does not (keksdose dev#546). */
58
+ weekday?: boolean;
59
+ }
60
+
61
+ /**
62
+ * What a date is rendered with, given the preference and the interface language
63
+ * (`language`: a code or tag — `"de"`, `"en"`, i18next's `resolvedLanguage`; it is read
64
+ * through `formatLocaleOf`, so English is British and an unshipped tag stays itself).
65
+ *
66
+ * `"auto"` keeps the short-month shape ("01 Aug 2026", "01. Aug. 2026"); every explicit
67
+ * choice is all-numeric, the only way it produces the shape its option promises —
68
+ * `month: "short"` under `sv-SE` would print "2026 aug. 01", not ISO.
69
+ */
70
+ export function resolveDateFormat(
71
+ preference: DateFormatPreference,
72
+ language: string | undefined,
73
+ options: DateFormatOptions = {},
74
+ ): ResolvedDateFormat {
75
+ const languageLocale = language ? formatLocaleOf(language) : undefined;
76
+ const weekday = options.weekday ? { weekday: { locale: languageLocale } } : {};
77
+ if (preference === "auto") {
78
+ return { locale: languageLocale, options: { year: "numeric", month: "short", day: "2-digit" }, ...weekday };
79
+ }
80
+ return { locale: preference, options: { year: "numeric", month: "2-digit", day: "2-digit" }, ...weekday };
81
+ }
82
+
83
+ /** A bare calendar day: no time, no zone. */
84
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
85
+
86
+ /**
87
+ * `dateText`, already rendered under the preference, with the day's NAME around it in
88
+ * `locale` (the language's). Not "weekday alone + comma": German says "So" alone and
89
+ * "So., 06.09.2026" in company. The full date is formatted in the language and split
90
+ * into parts; what that language puts before the first and after the last day/month/
91
+ * year part (the weekday, its comma) frames the preference's digits — so where the two
92
+ * locales coincide the output is the native string, character for character.
93
+ */
94
+ function withWeekday(date: Date, dateText: string, locale: string | undefined, options: Intl.DateTimeFormatOptions) {
95
+ const isDatePart = (p: Intl.DateTimeFormatPart) => p.type === "day" || p.type === "month" || p.type === "year";
96
+ const parts = new Intl.DateTimeFormat(locale, { weekday: "short", ...options }).formatToParts(date);
97
+ const first = parts.findIndex(isDatePart);
98
+ if (first === -1) return dateText;
99
+ let last = first;
100
+ parts.forEach((p, i) => {
101
+ if (isDatePart(p)) last = i;
102
+ });
103
+ const join = (ps: Intl.DateTimeFormatPart[]) => ps.map((p) => p.value).join("");
104
+ return join(parts.slice(0, first)) + dateText + join(parts.slice(last + 1));
105
+ }
106
+
107
+ /**
108
+ * A date under the preference: `""` for no value, the input itself when it does not
109
+ * parse. A bare "YYYY-MM-DD" is the calendar day it names in every zone (formatted in
110
+ * UTC, undoing `new Date`'s UTC-midnight parse — west of Greenwich it read a day early);
111
+ * a timestamp is an instant and stays in the reader's zone. The weekday, when asked for,
112
+ * is rendered in the same zone, so a Sunday is never captioned Saturday.
113
+ */
114
+ export function formatDateWithPreference(
115
+ value: string | Date | null | undefined,
116
+ preference: DateFormatPreference,
117
+ language: string | undefined,
118
+ options: DateFormatOptions = {},
119
+ ): string {
120
+ if (!value) return "";
121
+ const resolved = resolveDateFormat(preference, language, options);
122
+ const dateOnly = typeof value === "string" && DATE_ONLY.test(value);
123
+ const date = typeof value === "string" ? new Date(value) : value;
124
+ if (Number.isNaN(date.getTime())) return String(value);
125
+ const intl: Intl.DateTimeFormatOptions = dateOnly ? { ...resolved.options, timeZone: "UTC" } : resolved.options;
126
+ try {
127
+ const text = date.toLocaleDateString(resolved.locale, intl);
128
+ return resolved.weekday ? withWeekday(date, text, resolved.weekday.locale, intl) : text;
129
+ } catch {
130
+ // A language tag `Intl` refuses: the ISO day rather than nothing (keksdose's fallback).
131
+ return date.toISOString().slice(0, 10);
132
+ }
133
+ }
134
+
135
+ /** The sample an option is labelled with — 1 August 2026 in its shape, the shape being
136
+ * the whole choice (a country name would say nothing about the digit order). */
137
+ export function dateFormatSample(preference: DateFormatPreference, language: string | undefined): string {
138
+ return formatDateWithPreference("2026-08-01", preference, language);
139
+ }
140
+
141
+ /**
142
+ * The `<UiKitProvider formatDate>` for a preference — so the kit's own date fields
143
+ * (DatePicker, DateRangePicker, DateMark) write a day as the app's dates do. A day
144
+ * follows the preference, with the weekday where the kit suggests it; a month or a year
145
+ * answers `""` and keeps the kit's rendering in the language (rule 2). Rebuild it when
146
+ * the preference or the language changes (a `useMemo` on both).
147
+ */
148
+ export function dateFormatterFor(preference: DateFormatPreference, language: string | undefined): KitDateFormatter {
149
+ return (iso, { unit, weekday, locale }) =>
150
+ unit === "day" ? formatDateWithPreference(iso, preference, locale ?? language, { weekday }) : "";
151
+ }
package/src/lib/format.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import { useKitLocale } from "../i18n/kit-labels";
2
+ import { useKitLocale, useKitTimeZone } from "../i18n/kit-labels";
3
3
  import { parseIsoDate } from "./dates";
4
4
  import { roundHalfAway } from "./round";
5
5
 
@@ -182,10 +182,14 @@ const DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {
182
182
  };
183
183
 
184
184
  export interface FormatDateOptions extends BaseOptions {
185
- /** An IANA zone ("Europe/Zurich") for a timestamp. Left out: the device's. */
185
+ /** An IANA zone ("Europe/Zurich") for a timestamp. Left out: the device's. A bare
186
+ * "YYYY-MM-DD" ignores it: a calendar day is the day it names in every zone. */
186
187
  timeZone?: string;
187
188
  }
188
189
 
190
+ /** A bare ISO calendar day, `"YYYY-MM-DD"`. */
191
+ const BARE_ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
192
+
189
193
  /** What {@link formatDate} and {@link formatRelativeTime} accept. */
190
194
  export type DateInput = string | number | Date | null | undefined;
191
195
 
@@ -197,7 +201,7 @@ export type DateInput = string | number | Date | null | undefined;
197
201
  */
198
202
  export function toDate(value: DateInput): Date | null {
199
203
  if (value === null || value === undefined || value === "") return null;
200
- if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return parseIsoDate(value);
204
+ if (typeof value === "string" && BARE_ISO_DAY.test(value)) return parseIsoDate(value);
201
205
  const d = value instanceof Date ? value : new Date(value);
202
206
  return Number.isNaN(d.getTime()) ? null : d;
203
207
  }
@@ -229,10 +233,20 @@ export function formatDate(
229
233
  const d = toDate(value);
230
234
  if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;
231
235
  const base = typeof style === "string" ? DATE_STYLES[style] : style;
236
+ // A bare "YYYY-MM-DD" is a calendar day, which has no zone: it is the day it names
237
+ // wherever it is read, so a `timeZone` (in `options` or the style) never moves it — the
238
+ // device's local midnight shown in a zone west of it was the day before (round 0.45, N9).
239
+ if (typeof value === "string" && BARE_ISO_DAY.test(value)) {
240
+ const day = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
241
+ return dateFormatter(options.locale, { ...base, timeZone: "UTC" }).format(day);
242
+ }
232
243
  return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);
233
244
  }
234
245
 
235
246
  export interface FormatRelativeTimeOptions extends BaseOptions {
247
+ /** The zone of the date printed past `absoluteAfterDays` (0.46). Left out: the
248
+ * device's. */
249
+ timeZone?: string;
236
250
  /** The moment it is measured from. Default: now. */
237
251
  now?: DateInput;
238
252
  /** `"auto"` (default): "yesterday", "now", "next week". `"always"`: "1 day ago". */
@@ -337,16 +351,19 @@ export interface KitFormat {
337
351
  */
338
352
  export function useKitFormat(locale?: string): KitFormat {
339
353
  const kitLocale = useKitLocale(locale);
354
+ // 0.46 (§10.3): the provider's `timeZone` for timestamps; a call's own wins, and a bare
355
+ // "YYYY-MM-DD" ignores it (formatDate).
356
+ const timeZone = useKitTimeZone();
340
357
  return useMemo<KitFormat>(
341
358
  () => ({
342
359
  locale: kitLocale,
343
360
  formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),
344
361
  formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),
345
362
  formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),
346
- formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, ...options }),
347
- formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, ...options }),
363
+ formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, timeZone, ...options }),
364
+ formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, timeZone, ...options }),
348
365
  }),
349
- [kitLocale],
366
+ [kitLocale, timeZone],
350
367
  );
351
368
  }
352
369
 
@@ -0,0 +1,54 @@
1
+ import { useEffect } from "react";
2
+
3
+ import { settle } from "../account/account-parts";
4
+ import { deviceTimeZone } from "./time-zone-select";
5
+
6
+ export interface DeviceZoneReportOptions {
7
+ /** The account's `device_timezone` from `/auth/me`; `undefined` while it loads (no
8
+ * report until it is known). */
9
+ stored: string | null | undefined;
10
+ /** `PATCH /auth/me {device_timezone}` with this device's zone. */
11
+ report: (deviceTimezone: string) => Promise<unknown> | void;
12
+ /** A demo account: never reported (the server ignores it too, §10.3 Settled). */
13
+ isDemo?: boolean;
14
+ /** The signed-in account (its user id): the once-per-start guard is per account, so a
15
+ * second person signed in on a shared PC in the same start is reported too. Without it
16
+ * the guard is cleared when `stored` goes `undefined` (signed out). */
17
+ account?: string | number | null;
18
+ }
19
+
20
+ /** The (account, stored, device) triples already reported in this start of the app. */
21
+ const reportedThisStart = new Set<string>();
22
+
23
+ /**
24
+ * Report this device's zone to the account (0.46, §10.3): the account's zone follows the
25
+ * device last used, else `Europe/Zurich`; screens keep showing device time. Once per start
26
+ * of the app, and only when it differs from the stored `device_timezone` — a bench PC in Zurich never
27
+ * PATCHes on a sign-in — and never for a demo. Per account: a sign-out (`stored` back to
28
+ * `undefined`) clears the guard, and `account` keys it. The report IS the account's zone: it is
29
+ * automatic only (§10.3, amended 2026-10-20), so nothing is ever picked. A failed report
30
+ * is not retried in the same start; the next start tries again. Sign-up sends the zone
31
+ * itself (`RegisterRequest.device_timezone`).
32
+ *
33
+ * useDeviceZoneReport({ stored: me?.device_timezone, isDemo: me?.is_demo, account: me?.id,
34
+ * report: async (zone) => setUser((await api.patch("/auth/me", { device_timezone: zone })).data) });
35
+ */
36
+ export function useDeviceZoneReport({ stored, report, isDemo = false, account = null }: DeviceZoneReportOptions): void {
37
+ useEffect(() => {
38
+ if (stored === undefined) {
39
+ // Signed out (or still loading): the next account in this start is reported afresh.
40
+ reportedThisStart.clear();
41
+ return;
42
+ }
43
+ if (isDemo) return;
44
+ const device = deviceTimeZone();
45
+ if (device === stored) return;
46
+ const key = `${account ?? ""}|${stored ?? ""}→${device}`;
47
+ if (reportedThisStart.has(key)) return;
48
+ reportedThisStart.add(key);
49
+ void settle(() => report(device)).catch(() => undefined);
50
+ // `report` is the app's request function: a new closure each render must not report
51
+ // again, so only the values decide.
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [stored, isDemo, account]);
54
+ }
@@ -2,7 +2,7 @@
2
2
  // component code, as every `*-labels.ts`: the English catalogue imports them alone.
3
3
 
4
4
  /**
5
- * `feedbackNotificationTypes` — the setting words of the two feedback notification types
5
+ * `feedbackNotificationTypes` — the setting words of the feedback notification types
6
6
  * (docs/feedback-harmonization.md §9.11). In the kit, not in each app, because two apps
7
7
  * declare them (keksdose, Kurvenschmiede; kastlan `feedback_answered` alone). The mail and
8
8
  * push texts stay the app's.
@@ -14,6 +14,12 @@ export interface FeedbackNotificationTypesLabels {
14
14
  /** `feedback_comment`: a visible comment on such a report. */
15
15
  comment: string;
16
16
  commentDescription: string;
17
+ /** 0.46 (feedback §11): `feedback_received`, an admin's alert for a new report, a
18
+ * rework and a new crash. */
19
+ received: string;
20
+ receivedDescription: string;
21
+ /** A heading the apps may put over the admin's types ("Admin"). */
22
+ adminGroup: string;
17
23
  }
18
24
 
19
25
  export const DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS: FeedbackNotificationTypesLabels = {
@@ -21,4 +27,7 @@ export const DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS: FeedbackNotificationTyp
21
27
  answeredDescription: "When a report you wrote or commented on gets an answer.",
22
28
  comment: "Comments on reports you follow",
23
29
  commentDescription: "When someone comments on a report you wrote or commented on.",
30
+ received: "New feedback and reworks",
31
+ receivedDescription: "When someone files a report or sends one back.",
32
+ adminGroup: "Admin",
24
33
  };