@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.
- package/CHANGELOG.md +586 -0
- package/README.md +26 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -1
- package/dist/locales/ar.d.ts +100 -0
- package/dist/locales/ar.js +104 -3
- package/dist/locales/de.d.ts +100 -0
- package/dist/locales/de.js +104 -3
- package/dist/locales/en.d.ts +103 -3
- package/dist/locales/en.js +196 -3
- package/dist/locales/es.d.ts +100 -0
- package/dist/locales/es.js +111 -5
- package/dist/locales/fr.d.ts +100 -0
- package/dist/locales/fr.js +104 -3
- package/dist/locales/index.d.ts +1006 -6
- package/dist/locales/ja.d.ts +100 -0
- package/dist/locales/ja.js +104 -3
- package/dist/locales/ko.d.ts +100 -0
- package/dist/locales/ko.js +104 -3
- package/dist/locales/pt.d.ts +100 -0
- package/dist/locales/pt.js +104 -3
- package/dist/locales/ru.d.ts +100 -0
- package/dist/locales/ru.js +104 -3
- package/dist/locales/zh.d.ts +103 -3
- package/dist/locales/zh.js +103 -3
- package/dist/provider.d.ts +43 -1
- package/dist/provider.js +105 -3
- package/dist/useObjectLabel.js +21 -4
- package/dist/useSafeTranslation.js +17 -1
- package/package.json +2 -2
package/dist/locales/zh.js
CHANGED
|
@@ -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: '
|
|
733
|
-
writeStrippedByState: '
|
|
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;
|
package/dist/provider.d.ts
CHANGED
|
@@ -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)
|
package/dist/useObjectLabel.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
334
|
+
? resolve(viewSuffixes(objectName, viewName, 'emptyState.title'), fallback.title)
|
|
318
335
|
: fallback.title;
|
|
319
336
|
const message = fallback.message
|
|
320
|
-
? resolve(
|
|
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
|
|
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.
|
|
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.
|
|
40
|
+
"@types/react": "19.2.18",
|
|
41
41
|
"react": "19.2.8",
|
|
42
42
|
"typescript": "^6.0.3",
|
|
43
43
|
"vitest": "^4.1.10"
|