@object-ui/i18n 17.1.0 → 17.3.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.
@@ -84,6 +84,10 @@ const zh = {
84
84
  select: '选择...',
85
85
  openChat: '打开聊天',
86
86
  closeChat: '关闭聊天',
87
+ closePanel: '关闭面板',
88
+ resizeDrawer: '调整面板宽度',
89
+ itemCount: '{{count}} 项',
90
+ itemCountOne: '{{count}} 项',
87
91
  toggleSidebar: '切换侧边栏',
88
92
  package: '软件包',
89
93
  },
@@ -108,6 +112,7 @@ const zh = {
108
112
  url: '请输入有效的URL',
109
113
  pattern: '{{field}}格式不正确',
110
114
  formInvalid: '请检查表单中标记的字段:{{fields}}',
115
+ formInvalidJoiner: '、',
111
116
  unique: '{{field}}必须唯一',
112
117
  type: '{{field}}必须是有效的{{type}}',
113
118
  },
@@ -132,12 +137,21 @@ const zh = {
132
137
  stepOf: '第{{current}}步,共{{total}}步',
133
138
  createTitle: '新建{{object}}',
134
139
  editTitle: '编辑{{object}}',
140
+ viewTitle: '查看{{object}}',
141
+ createTargetOrg: '创建到{{org}}',
135
142
  saveRecord: '保存',
136
143
  create: '创建',
137
144
  update: '更新',
138
145
  createSuccess: '{{object}}创建成功',
139
146
  updateSuccess: '{{object}}更新成功',
140
147
  deleteSuccess: '{{object}}删除成功',
148
+ fullscreen: {
149
+ title: '编辑文本',
150
+ description: '编辑完整的文本内容,然后保存或取消更改。',
151
+ done: '完成',
152
+ toggle: '全屏编辑{{label}}',
153
+ textFallback: '文本',
154
+ },
141
155
  },
142
156
  fields: {
143
157
  relativeDate: {
@@ -177,6 +191,10 @@ const zh = {
177
191
  basicEditorHint: '富文本编辑器(基础)',
178
192
  placeholder: '请输入文字...',
179
193
  },
194
+ options: {
195
+ empty: '暂无可选项',
196
+ selectFirst: '请先选择{{fields}}',
197
+ },
180
198
  objectRef: {
181
199
  loading: '正在加载对象…',
182
200
  placeholder: '请选择对象',
@@ -204,6 +222,13 @@ const zh = {
204
222
  editAsJson: '以 JSON 编辑',
205
223
  useVisualBuilder: '使用可视化构建器',
206
224
  },
225
+ tags: {
226
+ placeholder: '输入后回车添加…',
227
+ },
228
+ textarea: {
229
+ characterCount: '已输入 {{count}} 个字符,最多 {{max}} 个',
230
+ charactersRemaining: '还可输入 {{count}} 个字符',
231
+ },
207
232
  },
208
233
  table: {
209
234
  rowsPerPage: '每页行数',
@@ -678,6 +703,12 @@ const zh = {
678
703
  over: '超载',
679
704
  empty: '没有可分配的任务。',
680
705
  },
706
+ quickFilter: {
707
+ all: '全部',
708
+ clear: '清除筛选',
709
+ empty: '无可选项',
710
+ resultSummary: '显示 {shown} / {total} 项任务',
711
+ },
681
712
  readOnly: '只读',
682
713
  readOnlyHint: '此视图已禁用编辑。',
683
714
  lockedHint: '无编辑权限',
@@ -728,11 +759,15 @@ const zh = {
728
759
  editInlineHint: '双击编辑',
729
760
  lockedByApproval: '审批中已锁定',
730
761
  lockedTooltip: '该记录有待审批的请求,编辑已被锁定',
731
- writeStrippedTitle: '部分字段未保存',
732
- writeStrippedReadonly: '以下字段为只读,未保存:{{fields}}',
733
- writeStrippedByState: '以下字段在记录当前状态下不可编辑,未保存:{{fields}}',
762
+ writeStrippedTitle: '已保存,但部分字段未生效',
763
+ writeStrippedReadonly: '以下字段为只读,未生效:{{fields}}',
764
+ writeStrippedByState: '以下字段在记录当前状态下不可编辑,未生效:{{fields}}',
734
765
  approvalPendingEditable: '审批中 · 可编辑',
735
766
  approvalPendingTooltip: '该记录有待审批的请求,但当前审批节点仍允许编辑',
767
+ approvalProgress: '审批 — 已通过 {{got}} / 共需 {{need}}',
768
+ approvalProgressGroups: '会签 — 已完成 {{got}} / 共 {{need}} 个组',
769
+ approvalProgressLabel: '审批进度',
770
+ approvalsPanelTitle: '审批',
736
771
  cancelApproval: '撤回审批',
737
772
  cancelApprovalInFlight: '撤回中…',
738
773
  cancelApprovalTooltip: '撤回当前的待审批请求以解除记录锁定',
@@ -761,6 +796,15 @@ const zh = {
761
796
  viewHistory: '查看历史',
762
797
  delete: '删除',
763
798
  moreActions: '更多操作',
799
+ addReaction: '添加表情回应',
800
+ pageHeaderActions: '页面标题栏操作',
801
+ emojiPicker: '表情选择器',
802
+ reactionCount: '{{emoji}} {{count}} 个回应',
803
+ reactionCountOne: '{{emoji}} {{count}} 个回应',
804
+ recordDetail: '记录详情',
805
+ recordDetailWithLabel: '{{label}}详情',
806
+ openAsFullPage: '以完整页面打开',
807
+ recordDetailOverlay: '{{title}} 的记录详情浮层。',
764
808
  addToFavorites: '添加到收藏',
765
809
  removeFromFavorites: '从收藏中移除',
766
810
  previousRecord: '上一条记录',
@@ -1404,6 +1448,13 @@ const zh = {
1404
1448
  retry: '重试',
1405
1449
  retrying: '正在重试…',
1406
1450
  },
1451
+ serverAction: {
1452
+ openingTitle: '正在打开…',
1453
+ openingBody: '正在打开…可能需要一点时间。',
1454
+ popupBlockedTitle: '浏览器拦截了弹窗',
1455
+ popupBlockedDescription: '浏览器阻止了新标签页的打开。',
1456
+ popupBlockedAction: '在新标签页打开',
1457
+ },
1407
1458
  shortcuts: {
1408
1459
  title: '键盘快捷键',
1409
1460
  description: '所有可用键盘快捷键的快速参考。',
@@ -1649,6 +1700,8 @@ const zh = {
1649
1700
  objectNotFound: '未找到对象',
1650
1701
  objectNotFoundDescription: '对象"{{objectName}}"在当前配置中不存在。',
1651
1702
  objectNotFoundHint: '请检查您的应用导航设置或从侧边栏选择其他对象。',
1703
+ filterOrNotSavable: '该筛选条件之间使用「或」,保存到视图暂不支持这种结构。它仍会作用于当前列表 —— 取消「或」分组后才能保存到视图。',
1704
+ filterNestedNotSavable: '该筛选使用了嵌套条件分组,保存到视图不支持这种结构。请改为单层条件列表后再保存到视图。',
1652
1705
  allRecords: '所有记录',
1653
1706
  exitDesignMode: '退出设计模式',
1654
1707
  enterDesignMode: '进入设计模式',
@@ -2648,6 +2701,19 @@ const zh = {
2648
2701
  addEnvironment: '新建环境',
2649
2702
  setUpProduction: '创建你的生产环境',
2650
2703
  addDevelopment: '新建开发环境',
2704
+ entitlement: {
2705
+ productionLimitTitle: '你已经有生产环境了',
2706
+ productionLimitBody: '每个组织只包含一个生产环境。如果还需要,可以新建一个组织,或者联系我们了解企业版方案。',
2707
+ planLockedTitle: '开发环境是付费功能',
2708
+ planLockedBody: '{{plan}}包含一个生产环境。升级后即可添加开发环境 —— 先在开发环境里搭建,确认无误再发布到生产环境。',
2709
+ limitTitle: '开发环境数量已达上限',
2710
+ limitBody: '容量随 AI 席位增长。增加一个 AI 席位,或归档一个不再使用的开发环境来腾出名额。',
2711
+ limitBodyWithCount: '开发环境已使用 {{used}}/{{limit}} 个。容量随 AI 席位增长 —— 增加一个 AI 席位,或归档一个不再使用的开发环境来腾出名额。',
2712
+ freePlan: '免费版',
2713
+ namedPlan: '{{plan}} 版',
2714
+ upgradeCta: '升级套餐',
2715
+ contactSalesCta: '联系销售',
2716
+ },
2651
2717
  },
2652
2718
  cloudConnection: {
2653
2719
  checking: '正在检查连接…',
@@ -3033,5 +3099,39 @@ const zh = {
3033
3099
  addSort: '添加排序',
3034
3100
  removeSort: '删除排序',
3035
3101
  },
3102
+ collaboration: {
3103
+ commentCount: '{{count}} 条评论',
3104
+ commentCountOne: '{{count}} 条评论',
3105
+ resolvedSuffix: ' · 已解决',
3106
+ sortComments: '评论排序',
3107
+ sortOldest: '最早优先',
3108
+ sortNewest: '最新优先',
3109
+ resolve: '标记已解决',
3110
+ reopen: '重新打开',
3111
+ justNow: '刚刚',
3112
+ minutesAgo: '{{count}} 分钟前',
3113
+ hoursAgo: '{{count}} 小时前',
3114
+ daysAgo: '{{count}} 天前',
3115
+ edited: '(已编辑)',
3116
+ reactionCount: '{{count}} 个回应',
3117
+ reactionCountOne: '{{count}} 个回应',
3118
+ addThumbsUp: '点赞',
3119
+ reply: '回复',
3120
+ reactThumbsUp: '以点赞回应',
3121
+ reactHeart: '以爱心回应',
3122
+ cancelReply: '取消回复',
3123
+ replyingTo: '正在回复 {{name}}…',
3124
+ replyingToComment: '正在回复该评论…',
3125
+ commentPlaceholder: '添加评论…(输入 @ 提及他人)',
3126
+ send: '发送',
3127
+ presentUserCount: '{{count}} 人在线',
3128
+ presentUserCountOne: '{{count}} 人在线',
3129
+ moreUserCount: '另有 {{count}} 人',
3130
+ moreUserCountOne: '另有 {{count}} 人',
3131
+ userStatusTitle: '{{name}}({{status}})',
3132
+ statusActive: '活跃',
3133
+ statusIdle: '空闲',
3134
+ statusAway: '离开',
3135
+ },
3036
3136
  };
3037
3137
  export default zh;
@@ -6,6 +6,28 @@
6
6
  import React from 'react';
7
7
  import type { i18n as I18nInstance } from 'i18next';
8
8
  import { type I18nConfig } from './i18n';
9
+ /**
10
+ * `localStorage` key holding the user's explicit language choice.
11
+ *
12
+ * Follows the console's existing preference-key convention
13
+ * (`objectui-favorites`, `objectui-recent-items`), and sits beside the theme
14
+ * preference the same avatar menu writes — a language switch is a preference,
15
+ * not ephemeral UI state, so it belongs in storage (AGENTS.md §5 #8).
16
+ *
17
+ * Exported so an app that builds its own i18next instance (and therefore owns
18
+ * its bootstrap language — see {@link I18nProviderProps.instance}) can honour
19
+ * the same preference, and so a sign-out flow can clear it.
20
+ */
21
+ export declare const LOCALE_STORAGE_KEY = "objectui-locale";
22
+ /**
23
+ * Read the persisted language choice, or `null` when nothing is stored.
24
+ *
25
+ * Defensive by design: `localStorage` does not exist under SSR and *throws* on
26
+ * access in Safari private mode / partitioned iframes. A language preference is
27
+ * never worth taking the app down for, so every failure degrades to "nothing
28
+ * stored".
29
+ */
30
+ export declare function readStoredLanguage(): string | null;
9
31
  interface I18nContextValue {
10
32
  /** Current language code */
11
33
  language: string;
@@ -39,6 +61,26 @@ export interface I18nProviderProps {
39
61
  * ```
40
62
  */
41
63
  loadLanguage?: (lang: string) => Promise<Record<string, unknown>>;
64
+ /**
65
+ * Remember the active language in `localStorage` ({@link LOCALE_STORAGE_KEY})
66
+ * and restore it on the next mount. Default: `true`.
67
+ *
68
+ * Every language change on the instance is persisted — whether it came from
69
+ * this provider's `changeLanguage`, a switcher calling
70
+ * `i18n.changeLanguage()` directly, or app code — because the promise the UI
71
+ * makes ("this is my language now") must not depend on which API the caller
72
+ * reached for.
73
+ *
74
+ * The *restore* half only applies to an instance this provider creates: when
75
+ * you pass your own {@link I18nProviderProps.instance}, its bootstrap
76
+ * language is yours to choose (call {@link readStoredLanguage} if you want
77
+ * the same preference honoured).
78
+ *
79
+ * Set `false` for surfaces that must stay on a fixed language regardless of
80
+ * what the user picked elsewhere on the origin — previews, demos, screenshot
81
+ * harnesses.
82
+ */
83
+ persistLanguage?: boolean;
42
84
  /** Children to render */
43
85
  children: React.ReactNode;
44
86
  }
@@ -52,7 +94,7 @@ export interface I18nProviderProps {
52
94
  * </I18nProvider>
53
95
  * ```
54
96
  */
55
- export declare function I18nProvider({ config, instance: externalInstance, loadLanguage, children }: I18nProviderProps): React.FunctionComponentElement<React.ProviderProps<I18nContextValue | null>>;
97
+ export declare function I18nProvider({ config, instance: externalInstance, loadLanguage, persistLanguage, children, }: I18nProviderProps): React.FunctionComponentElement<React.ProviderProps<I18nContextValue | null>>;
56
98
  /**
57
99
  * Hook to access Object UI i18n context
58
100
  *
package/dist/provider.js CHANGED
@@ -6,6 +6,100 @@
6
6
  import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
7
7
  import { I18nextProvider, useTranslation } from 'react-i18next';
8
8
  import { createI18n, getDirection } from './i18n';
9
+ import { builtInLocales } from './locales/index';
10
+ /**
11
+ * `localStorage` key holding the user's explicit language choice.
12
+ *
13
+ * Follows the console's existing preference-key convention
14
+ * (`objectui-favorites`, `objectui-recent-items`), and sits beside the theme
15
+ * preference the same avatar menu writes — a language switch is a preference,
16
+ * not ephemeral UI state, so it belongs in storage (AGENTS.md §5 #8).
17
+ *
18
+ * Exported so an app that builds its own i18next instance (and therefore owns
19
+ * its bootstrap language — see {@link I18nProviderProps.instance}) can honour
20
+ * the same preference, and so a sign-out flow can clear it.
21
+ */
22
+ export const LOCALE_STORAGE_KEY = 'objectui-locale';
23
+ /**
24
+ * Read the persisted language choice, or `null` when nothing is stored.
25
+ *
26
+ * Defensive by design: `localStorage` does not exist under SSR and *throws* on
27
+ * access in Safari private mode / partitioned iframes. A language preference is
28
+ * never worth taking the app down for, so every failure degrades to "nothing
29
+ * stored".
30
+ */
31
+ export function readStoredLanguage() {
32
+ if (typeof window === 'undefined')
33
+ return null;
34
+ try {
35
+ return window.localStorage.getItem(LOCALE_STORAGE_KEY);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function writeStoredLanguage(lang) {
42
+ if (typeof window === 'undefined')
43
+ return;
44
+ try {
45
+ window.localStorage.setItem(LOCALE_STORAGE_KEY, lang);
46
+ }
47
+ catch {
48
+ // Storage blocked or full — the switch still applies for this session.
49
+ }
50
+ }
51
+ function clearStoredLanguage() {
52
+ if (typeof window === 'undefined')
53
+ return;
54
+ try {
55
+ window.localStorage.removeItem(LOCALE_STORAGE_KEY);
56
+ }
57
+ catch {
58
+ // Nothing readable means nothing to purge.
59
+ }
60
+ }
61
+ /**
62
+ * Languages `createI18n(config)` will know about: the built-in packs plus any
63
+ * extra `config.resources`.
64
+ *
65
+ * `hasOwnProperty` rather than `in`: a junk stored value like `constructor`
66
+ * would pass an `in` check against the locale map's prototype.
67
+ */
68
+ function isKnownLanguage(lang, config) {
69
+ const own = Object.prototype.hasOwnProperty;
70
+ return own.call(builtInLocales, lang) || Boolean(config?.resources && own.call(config.resources, lang));
71
+ }
72
+ /**
73
+ * Resolve the bootstrap config for a provider-owned i18next instance, applying
74
+ * a stored language choice when there is a usable one.
75
+ *
76
+ * Applied at instance creation rather than through a post-mount
77
+ * `changeLanguage`, because the very first render must already be in the right
78
+ * locale: `<html lang>` seeds the `Accept-Language` header on every API call
79
+ * (`createAuthenticatedFetch`, issue #1319), so a late switch would fetch the
80
+ * first wave of server-resolved metadata labels in the wrong language and then
81
+ * have to remount to fix it.
82
+ *
83
+ * A stored choice outranks browser detection (which itself outranks
84
+ * `defaultLanguage` in `createI18n`) — otherwise a user who picked 中文 on a
85
+ * `ja` browser would be handed `ja` back on every reload, and the preference
86
+ * would be unusable for exactly the users who need it.
87
+ *
88
+ * A stored value the app no longer offers is dropped *and purged*, so one stale
89
+ * entry can never lock the UI to a locale that has no translations.
90
+ */
91
+ function resolveBootstrapConfig(config, persist) {
92
+ if (!persist)
93
+ return config;
94
+ const stored = readStoredLanguage();
95
+ if (!stored)
96
+ return config;
97
+ if (!isKnownLanguage(stored, config)) {
98
+ clearStoredLanguage();
99
+ return config;
100
+ }
101
+ return { ...config, defaultLanguage: stored, detectBrowserLanguage: false };
102
+ }
9
103
  const ObjectI18nContext = createContext(null);
10
104
  /**
11
105
  * I18nProvider - Wraps your app with i18n support
@@ -17,8 +111,8 @@ const ObjectI18nContext = createContext(null);
17
111
  * </I18nProvider>
18
112
  * ```
19
113
  */
20
- export function I18nProvider({ config, instance: externalInstance, loadLanguage, children }) {
21
- const i18nInstance = useMemo(() => externalInstance || createI18n(config), [externalInstance, config]);
114
+ export function I18nProvider({ config, instance: externalInstance, loadLanguage, persistLanguage = true, children, }) {
115
+ const i18nInstance = useMemo(() => externalInstance || createI18n(resolveBootstrapConfig(config, persistLanguage)), [externalInstance, config, persistLanguage]);
22
116
  const [language, setLanguage] = useState(i18nInstance.language || 'en');
23
117
  const direction = getDirection(language);
24
118
  // Track which languages have had app-specific translations loaded
@@ -27,6 +121,14 @@ export function I18nProvider({ config, instance: externalInstance, loadLanguage,
27
121
  useEffect(() => {
28
122
  const handleLanguageChanged = (lng) => {
29
123
  setLanguage(lng);
124
+ // Remember the choice. This is the single choke point every switch runs
125
+ // through — i18next fires `languageChanged` for the context's
126
+ // `changeLanguage` and for a direct `i18n.changeLanguage()` alike — so no
127
+ // switcher can be wired in a way that silently forgets the preference.
128
+ // Bootstrap does NOT fire this event, so restoring a stored language
129
+ // never re-writes it.
130
+ if (persistLanguage)
131
+ writeStoredLanguage(lng);
30
132
  // Update document direction for RTL support
31
133
  if (typeof document !== 'undefined') {
32
134
  document.documentElement.dir = getDirection(lng);
@@ -49,7 +151,7 @@ export function I18nProvider({ config, instance: externalInstance, loadLanguage,
49
151
  return () => {
50
152
  i18nInstance.off('languageChanged', handleLanguageChanged);
51
153
  };
52
- }, [i18nInstance]);
154
+ }, [i18nInstance, persistLanguage]);
53
155
  // Load app-specific translations for the initial language on mount
54
156
  useEffect(() => {
55
157
  if (!loadLanguage)
@@ -185,6 +185,23 @@ export function useObjectLabel() {
185
185
  ? [`reports.${reportName}.${tail}`, `reports.${base}.${tail}`]
186
186
  : [`reports.${reportName}.${tail}`];
187
187
  };
188
+ /**
189
+ * Build suffix candidates for a list-view scoped key.
190
+ *
191
+ * The runtime uses qualified view ids (`<objectName>.<viewName>`) in URLs
192
+ * and metadata records, while translation bundles are keyed by the authored
193
+ * view name under `_views`. Resolve the unqualified name first so every
194
+ * surface can pass its canonical id without leaking the metadata fallback.
195
+ */
196
+ const viewSuffixes = (objectName, viewName, tail) => {
197
+ const objectNames = [objectName, stripNamespace(objectName)];
198
+ const matchedPrefix = objectNames
199
+ .map((name) => `${name}.`)
200
+ .find((prefix) => viewName.startsWith(prefix));
201
+ const shortViewName = matchedPrefix ? viewName.slice(matchedPrefix.length) : viewName;
202
+ const viewNames = shortViewName === viewName ? [viewName] : [shortViewName, viewName];
203
+ return viewNames.flatMap((name) => objectSuffixes(objectName, `_views.${name}.${tail}`));
204
+ };
188
205
  return {
189
206
  /**
190
207
  * Resolve translated object label, falling back to objectDef.label.
@@ -294,14 +311,14 @@ export function useObjectLabel() {
294
311
  * Resolve translated list-view label.
295
312
  * Convention (per @objectstack/spec): `{ns}.objects.{objectName}._views.{viewName}.label`.
296
313
  */
297
- viewLabel: (objectName, viewName, fallback) => resolve(objectSuffixes(objectName, `_views.${viewName}.label`), fallback),
314
+ viewLabel: (objectName, viewName, fallback) => resolve(viewSuffixes(objectName, viewName, 'label'), fallback),
298
315
  /**
299
316
  * Resolve translated list-view description.
300
317
  * Convention: `{ns}.objects.{objectName}._views.{viewName}.description`.
301
318
  */
302
319
  viewDescription: (objectName, viewName, fallback) => {
303
320
  const fb = fallback ?? '';
304
- const resolved = resolve(objectSuffixes(objectName, `_views.${viewName}.description`), fb);
321
+ const resolved = resolve(viewSuffixes(objectName, viewName, 'description'), fb);
305
322
  return resolved || undefined;
306
323
  },
307
324
  /**
@@ -314,10 +331,10 @@ export function useObjectLabel() {
314
331
  if (!fallback)
315
332
  return undefined;
316
333
  const title = fallback.title
317
- ? resolve(objectSuffixes(objectName, `_views.${viewName}.emptyState.title`), fallback.title)
334
+ ? resolve(viewSuffixes(objectName, viewName, 'emptyState.title'), fallback.title)
318
335
  : fallback.title;
319
336
  const message = fallback.message
320
- ? resolve(objectSuffixes(objectName, `_views.${viewName}.emptyState.message`), fallback.message)
337
+ ? resolve(viewSuffixes(objectName, viewName, 'emptyState.message'), fallback.message)
321
338
  : fallback.message;
322
339
  return { ...fallback, title, message };
323
340
  },
@@ -17,7 +17,23 @@ export function createSafeTranslation(defaults, testKey) {
17
17
  let value = defaults[key] || key;
18
18
  if (options) {
19
19
  for (const [k, v] of Object.entries(options)) {
20
- value = value.replace(`{{${k}}}`, String(v));
20
+ // `split(needle).join(value)` — deliberately not `replace`, and not
21
+ // `replaceAll` either (objectui#3418). This path must agree with
22
+ // i18next, which serves the *provider* path; any divergence is a
23
+ // silent fork that only shows up on provider-less hosts, where we are
24
+ // least likely to see it:
25
+ // 1. `replace` with a string needle substitutes only the FIRST
26
+ // occurrence. i18next substitutes every one, so a sentence that
27
+ // repeats a placeholder ("Selected {{count}} of {{count}} items"
28
+ // — natural in many locales, and often required by RTL /
29
+ // agglutinative word order) leaked literal braces to users.
30
+ // 2. `replace` AND `replaceAll` both interpret `$&`, `` $` ``, `$'`
31
+ // and `$$` in the *replacement* string. i18next does not. Values
32
+ // here are runtime data — record labels, search terms — so this
33
+ // one is reachable today, unlike (1).
34
+ // split/join is literal on both sides, which is exactly i18next's
35
+ // behaviour, and needs no regex escaping of the placeholder name.
36
+ value = value.split(`{{${k}}}`).join(String(v));
21
37
  }
22
38
  }
23
39
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/i18n",
3
- "version": "17.1.0",
3
+ "version": "17.3.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",
@@ -37,7 +37,7 @@
37
37
  "react": "^18.0.0 || ^19.0.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/react": "19.2.17",
40
+ "@types/react": "19.2.18",
41
41
  "react": "19.2.8",
42
42
  "typescript": "^6.0.3",
43
43
  "vitest": "^4.1.10"