@jx3box/jx3box-ui 2.3.21 → 2.3.22

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.
@@ -486,7 +486,9 @@ export default {
486
486
  migrateCommunity: { title: "遷回原版塊", postTitle: "標題", author: "作者", originalSection: "原版塊", originalLink: "原連結", notAvailable: "此貼文不可遷移,不存在原貼文", confirm: "確認將此貼文遷回原版塊?", success: "遷移成功" },
487
487
  moveCommunity: { title: "遷移至論壇", category: "分類", categoryPlaceholder: "請選擇貼文分類", introduction: "簡介", contentPlaceholder: "請輸入內容", images: "附圖", irreversibleTip: "請確認此操作不可逆", irreversible: "我已確認此操作不可逆", missingPost: "文章ID不存在!", selectCategory: "請選擇分類!" },
488
488
  wiki: { loading: "🎉 資料載入中...", loadFailed: "⚠️ 資料載入異常", empty: "💧 暫無資料", reply: "回覆", nickname: "暱稱:", mysterious: "神祕俠士" },
489
- wikiPanel: { contributors: "參與貢獻", difficulty: "綜合難度", popularity: "熱度", updatedAt: "更新時間" },
489
+ wikiPanel: { contributors: "參與貢獻", difficulty: "綜合難度", popularity: "熱度", updatedAt: "更新時間", qrTrigger: "QR Code", qrScanToVisit: "掃描即可瀏覽" },
490
490
  wikiComments: { title: "百科評論", empty: "💧 暫無評論", contentRequired: "請先填寫評論內容再嘗試提交", submitSuccess: "提交成功,請等待審核" },
491
491
  wikiRevisions: { title: "歷史版本", version: "版本", contributor: "貢獻者", remark: "修訂說明" },
492
+ wikiComment: { top: "置頂", star: "精華", collapse: "收起", setStar: "設為精華", cancelStar: "取消精華", setTop: "置頂", cancelTop: "取消置頂", publishUnavailable: "發布評論異常,請聯絡管理員", actionUnavailable: "操作異常,請聯絡管理員" },
493
+ gamePrice: { zhuan: "磚", jin: "金", yin: "銀", tong: "銅" },
492
494
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-ui",
3
- "version": "2.3.21",
3
+ "version": "2.3.22",
4
4
  "description": "JX3BOX Vue3 UI",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -13,6 +13,7 @@
13
13
  "build:dev": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vue-cli-service build --mode development",
14
14
  "build:prod": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vue-cli-service build --mode production",
15
15
  "lint": "vue-cli-service lint",
16
+ "test:i18n": "node --test tests/wiki-i18n-contract.test.js",
16
17
  "update": "npm --registry https://registry.npmjs.org install @jx3box/jx3box-common@latest @jx3box/jx3box-data@latest @jx3box/jx3box-editor@latest @jx3box/jx3box-emotion@latest @jx3box/jx3box-macro@latest @jx3box/jx3box-talent@latest"
17
18
  },
18
19
  "eslintConfig": {
package/src/App.vue CHANGED
@@ -111,27 +111,36 @@
111
111
  </el-radio-group>
112
112
  <singlebox :post="post" />
113
113
  </el-tab-pane>
114
- <el-tab-pane label="百科组件" name="wiki">
114
+ <el-tab-pane class="m-pvx-wiki-demo" label="百科组件" name="wiki">
115
115
  <h1 class="m-title">游戏价格组件</h1>
116
- <div class="m-block">
117
- <GamePrice :price="100009999" :align="true"></GamePrice>
116
+ <div class="m-block m-game-price-demo">
117
+ <GamePrice
118
+ :price="100009999"
119
+ :align="true"
120
+ variant="surface"
121
+ ></GamePrice>
122
+ <GamePrice
123
+ :price="-20800"
124
+ variant="surface"
125
+ style="--jx3box-ui-game-price-negative-color: #e11d48"
126
+ ></GamePrice>
118
127
  </div>
119
128
 
120
- <h1 class="m-title">百科面板</h1>
121
129
  <div class="m-block">
122
- <WikiPanel :wiki-post="wiki_post">
123
- <template #head-title>奇遇攻略</template>
130
+ <WikiPanel :wiki-post="wiki_post" variant="surface">
131
+ <template #head-title>
132
+ <i class="u-icon el-icon-document"></i>
133
+ <span class="u-txt">奇遇攻略</span>
134
+ </template>
124
135
  </WikiPanel>
125
136
  </div>
126
137
 
127
- <h1 class="m-title">百科评论</h1>
128
138
  <div class="m-block">
129
- <WikiComments :source-id="12572" type="achievement" />
139
+ <WikiComments :source-id="12572" type="achievement" variant="surface" />
130
140
  </div>
131
141
 
132
- <h1 class="m-title">百科修订</h1>
133
142
  <div class="m-block">
134
- <WikiRevisions type="achievement" :source-id="12572" />
143
+ <WikiRevisions type="achievement" :source-id="12572" variant="surface" />
135
144
  </div>
136
145
  </el-tab-pane>
137
146
  <el-tab-pane label="移动组件" name="mobile">
@@ -379,4 +388,38 @@ export default {
379
388
  flex-direction: column;
380
389
  gap: 20px;
381
390
  }
391
+
392
+ .m-pvx-wiki-demo {
393
+ padding: 24px;
394
+ background: #f6f8fa;
395
+
396
+ > .m-title:first-child {
397
+ margin-top: 0;
398
+ }
399
+
400
+ > .m-block {
401
+ padding: 0;
402
+ }
403
+
404
+ > .m-block + .m-block {
405
+ margin-top: 24px;
406
+ }
407
+ }
408
+
409
+ .m-game-price-demo {
410
+ display: flex;
411
+ flex-wrap: wrap;
412
+ align-items: flex-start;
413
+ gap: 20px;
414
+ }
415
+
416
+ @media screen and (max-width: 720px) {
417
+ .m-pvx-wiki-demo {
418
+ padding: 16px;
419
+
420
+ > .m-block + .m-block {
421
+ margin-top: 20px;
422
+ }
423
+ }
424
+ }
382
425
  </style>
@@ -1,12 +1,18 @@
1
1
  <template>
2
2
  <div ref="trigger" class="w-qrcode" v-bind="$attrs" @click="togglePic" v-if="mode == 'cms'">
3
3
  <img class="u-icon" svg-inline src="../../assets/img/widget/qr-code.svg" />
4
- <span class="u-text">二维码</span>
4
+ <span class="u-text">{{ triggerText }}</span>
5
5
  </div>
6
6
  <teleport to="body">
7
- <div v-if="mode == 'cms' && active" class="u-qrcode u-qrcode-popup w-qrcode-static" :style="popupStyle" @click.stop>
7
+ <div
8
+ v-if="mode == 'cms' && active"
9
+ class="u-qrcode u-qrcode-popup w-qrcode-static"
10
+ :class="popupClass"
11
+ :style="popupStyle"
12
+ @click.stop
13
+ >
8
14
  <qrcode-vue class="u-pic" :value="value" :size="size" level="H"></qrcode-vue>
9
- <span class="u-txt">扫一扫即可访问</span>
15
+ <span class="u-txt">{{ popupText }}</span>
10
16
  </div>
11
17
  </teleport>
12
18
  <div class="w-qrcode-static" v-bind="$attrs" v-if="mode == 'static'">
@@ -38,6 +44,18 @@ export default {
38
44
  type: Number,
39
45
  default: 100,
40
46
  },
47
+ triggerText: {
48
+ type: String,
49
+ default: "二维码",
50
+ },
51
+ popupText: {
52
+ type: String,
53
+ default: "扫一扫即可访问",
54
+ },
55
+ popupClass: {
56
+ type: [String, Array, Object],
57
+ default: "",
58
+ },
41
59
  },
42
60
  data: function () {
43
61
  return {
@@ -1,11 +1,11 @@
1
1
  <template>
2
- <span class="c-game-price" :class="alignClass">
2
+ <span class="c-game-price" :class="rootClasses">
3
3
  <span class="u-neg" v-if="numericPrice < 0">- </span>
4
4
  <span class="u-price">
5
5
  <template v-for="part in priceParts" :key="part.unit">
6
6
  <span class="u-part" :class="`u-${part.unit}`">
7
7
  <span class="u-price-value">{{ part.value }}</span>
8
- <img :src="part.icon" :alt="part.alt" />
8
+ <img :src="part.icon" :alt="$jx3boxT(`jx3boxUi.gamePrice.${part.unit}`, part.alt)" />
9
9
  </span>
10
10
  </template>
11
11
  </span>
@@ -36,16 +36,38 @@ export default {
36
36
  type: [Boolean, String],
37
37
  default: false,
38
38
  },
39
+ variant: {
40
+ type: String,
41
+ default: "default",
42
+ validator: (value) => ["default", "surface", "plain"].includes(value),
43
+ },
44
+ size: {
45
+ type: String,
46
+ default: "medium",
47
+ validator: (value) => ["small", "medium", "large"].includes(value),
48
+ },
49
+ layout: {
50
+ type: String,
51
+ default: "inline",
52
+ validator: (value) => ["inline", "block"].includes(value),
53
+ },
54
+ wrap: {
55
+ type: Boolean,
56
+ default: true,
57
+ },
39
58
  },
40
59
  computed: {
41
60
  numericPrice() {
42
61
  const value = Number(this.price);
43
62
  return Number.isFinite(value) ? Math.trunc(value) : 0;
44
63
  },
45
- alignClass() {
46
- if (this.align === true) return "is-align";
47
- if (typeof this.align === "string" && this.align) return `is-${this.align}`;
48
- return "";
64
+ rootClasses() {
65
+ const classes = [`is-${this.variant}`, `is-size-${this.size}`, `is-layout-${this.layout}`];
66
+ if (this.align === true) classes.push("is-align");
67
+ if (typeof this.align === "string" && this.align) classes.push(`is-${this.align}`);
68
+ if (!this.wrap) classes.push("is-nowrap");
69
+ if (this.numericPrice < 0) classes.push("is-negative");
70
+ return classes;
49
71
  },
50
72
  priceParts() {
51
73
  const value = Math.abs(this.numericPrice);
@@ -75,21 +97,28 @@ export default {
75
97
 
76
98
  <style lang="less">
77
99
  .c-game-price {
100
+ --jx3box-ui-game-price-color: #333;
101
+ --jx3box-ui-game-price-icon-size: 18px;
102
+ --jx3box-ui-game-price-font-size: 12px;
103
+ --jx3box-ui-game-price-gap: 6px;
104
+ --jx3box-ui-game-price-negative-color: var(--jx3box-ui-game-price-color);
105
+
78
106
  display: inline-flex;
79
107
  align-items: center;
80
- color: #333;
108
+ color: var(--jx3box-ui-game-price-color);
81
109
  font-weight: 500;
82
110
 
83
111
  .u-price {
84
112
  display: inline-flex;
85
113
  align-items: center;
86
114
  flex-wrap: wrap;
87
- gap: 6px;
115
+ gap: var(--jx3box-ui-game-price-gap);
88
116
  }
89
117
 
90
118
  .u-neg {
91
119
  display: inline-flex;
92
120
  align-items: center;
121
+ color: var(--jx3box-ui-game-price-negative-color);
93
122
  }
94
123
 
95
124
  .u-part {
@@ -100,14 +129,15 @@ export default {
100
129
  }
101
130
 
102
131
  img {
103
- width: 18px;
104
- height: 18px;
132
+ width: var(--jx3box-ui-game-price-icon-size);
133
+ height: var(--jx3box-ui-game-price-icon-size);
105
134
  object-fit: contain;
106
135
  vertical-align: middle;
107
136
  }
108
137
 
109
138
  .u-price-value {
110
- .fz(12px,18px);
139
+ font-size: var(--jx3box-ui-game-price-font-size);
140
+ line-height: var(--jx3box-ui-game-price-icon-size);
111
141
  }
112
142
 
113
143
  &.is-align {
@@ -130,5 +160,32 @@ export default {
130
160
  justify-content: flex-end;
131
161
  width: 100%;
132
162
  }
163
+
164
+ &.is-size-small {
165
+ --jx3box-ui-game-price-icon-size: 14px;
166
+ --jx3box-ui-game-price-font-size: 11px;
167
+ --jx3box-ui-game-price-gap: 4px;
168
+ }
169
+
170
+ &.is-size-large {
171
+ --jx3box-ui-game-price-icon-size: 22px;
172
+ --jx3box-ui-game-price-font-size: 14px;
173
+ --jx3box-ui-game-price-gap: 8px;
174
+ }
175
+
176
+ &.is-layout-block {
177
+ display: flex;
178
+ width: 100%;
179
+ }
180
+
181
+ &.is-nowrap {
182
+ .u-price {
183
+ flex-wrap: nowrap;
184
+ }
185
+ }
186
+
187
+ &.is-surface {
188
+ --jx3box-ui-game-price-color: #334155;
189
+ }
133
190
  }
134
191
  </style>
@@ -11,7 +11,7 @@
11
11
  v-text="comment.user_nickname"
12
12
  ></a>
13
13
  <template v-if="comment.parent_id">
14
- <span>&nbsp;回复&nbsp;</span>
14
+ <span>&nbsp;{{ $jx3boxT("jx3boxUi.wiki.reply", "回复") }}&nbsp;</span>
15
15
  <a
16
16
  class="u-nickname"
17
17
  :href="comment.parent.user_id ? author_url(comment.parent.user_id) : null"
@@ -21,11 +21,11 @@
21
21
  </template>
22
22
  <span class="u-mark u-top" v-if="comment.is_top">
23
23
  <i class="el-icon-download"></i>
24
- 置顶
24
+ {{ $jx3boxT("jx3boxUi.wikiComment.top", "置顶") }}
25
25
  </span>
26
26
  <span class="u-mark u-star" v-if="comment.is_star">
27
27
  <i class="el-icon-star-on"></i>
28
- 精华
28
+ {{ $jx3boxT("jx3boxUi.wikiComment.star", "精华") }}
29
29
  </span>
30
30
  </div>
31
31
  <p class="u-content" v-html="comment.content"></p>
@@ -38,9 +38,11 @@
38
38
  class="u-reply"
39
39
  @click="comment.reply_form.show = !comment.reply_form.show"
40
40
  size="small"
41
+ :aria-expanded="true"
42
+ :aria-controls="`wiki-comment-reply-${comment.id}`"
41
43
  >
42
44
  <i class="el-icon-arrow-up"></i>
43
- <span>收起</span>
45
+ <span>{{ $jx3boxT("jx3boxUi.wikiComment.collapse", "收起") }}</span>
44
46
  </el-button>
45
47
  <el-button
46
48
  type="primary"
@@ -50,8 +52,10 @@
50
52
  @click="comment.reply_form.show = !comment.reply_form.show"
51
53
  icon="ChatDotRound"
52
54
  size="small"
55
+ :aria-expanded="false"
56
+ :aria-controls="`wiki-comment-reply-${comment.id}`"
53
57
  >
54
- <span>回复</span>
58
+ <span>{{ $jx3boxT("jx3boxUi.wiki.reply", "回复") }}</span>
55
59
  </el-button>
56
60
  <template v-if="isEditor && !comment.parent_id">
57
61
  <el-button
@@ -61,25 +65,43 @@
61
65
  plain
62
66
  :icon="comment.is_star ? 'StarFilled' : 'Star'"
63
67
  size="small"
64
- >{{ comment.is_star ? "取消加精" : "加精" }}</el-button
68
+ >{{
69
+ comment.is_star
70
+ ? $jx3boxT("jx3boxUi.wikiComment.cancelStar", "取消加精")
71
+ : $jx3boxT("jx3boxUi.wikiComment.setStar", "加精")
72
+ }}</el-button
65
73
  >
66
74
  <el-button type="primary" class="u-reply" @click="onTop(comment)" plain icon="Top" size="small">{{
67
- comment.is_top ? "取消置顶" : "置顶"
75
+ comment.is_top
76
+ ? $jx3boxT("jx3boxUi.wikiComment.cancelTop", "取消置顶")
77
+ : $jx3boxT("jx3boxUi.wikiComment.setTop", "置顶")
68
78
  }}</el-button>
69
79
  </template>
70
80
  <!-- 更新时间 -->
71
81
  <span class="u-time" v-text="ts2str(comment.updated)"></span>
72
82
  </div>
73
83
  <!-- 评论回复表单 -->
74
- <div class="m-reply-form" v-if="comment.reply_form && comment.reply_form.show">
75
- <textarea class="u-reply-content" v-model="comment.reply_form.content"></textarea>
84
+ <div
85
+ class="m-reply-form"
86
+ :id="`wiki-comment-reply-${comment.id}`"
87
+ v-if="comment.reply_form && comment.reply_form.show"
88
+ >
89
+ <textarea
90
+ class="u-reply-content"
91
+ v-model="comment.reply_form.content"
92
+ :aria-label="$jx3boxT('jx3boxUi.wiki.reply', '回复')"
93
+ ></textarea>
76
94
  <div class="u-author">
77
- <span>昵称:</span>
78
- <input v-model="comment.reply_form.user_nickname" type="text" />
95
+ <span>{{ $jx3boxT("jx3boxUi.wiki.nickname", "昵称:") }}</span>
96
+ <input
97
+ v-model="comment.reply_form.user_nickname"
98
+ type="text"
99
+ :aria-label="$jx3boxT('jx3boxUi.wiki.nickname', '昵称:')"
100
+ />
79
101
  </div>
80
102
  <el-button type="primary" class="u-submit" @click="create_comment(comment.reply_form, comment.id)">
81
103
  <i class="el-icon-check"></i>
82
- <span>提交</span>
104
+ <span>{{ $jx3boxT("jx3boxUi.common.submit", "提交") }}</span>
83
105
  </el-button>
84
106
  </div>
85
107
  </div>
@@ -117,7 +139,10 @@ export default {
117
139
  if (!app.create_comment) app = app.$parent;
118
140
  if (!app.create_comment) {
119
141
  this.$message({
120
- message: "发布评论异常,请联系管理员",
142
+ message: this.$jx3boxT(
143
+ "jx3boxUi.wikiComment.publishUnavailable",
144
+ "发布评论异常,请联系管理员"
145
+ ),
121
146
  type: "warning",
122
147
  });
123
148
  return;
@@ -132,7 +157,10 @@ export default {
132
157
  if (!app.star_comment) app = app.$parent;
133
158
  if (!app.star_comment) {
134
159
  this.$message({
135
- message: "操作异常,请联系管理员",
160
+ message: this.$jx3boxT(
161
+ "jx3boxUi.wikiComment.actionUnavailable",
162
+ "操作异常,请联系管理员"
163
+ ),
136
164
  type: "warning",
137
165
  });
138
166
  return;
@@ -144,7 +172,10 @@ export default {
144
172
  if (!app.top_comment) app = app.$parent;
145
173
  if (!app.top_comment) {
146
174
  this.$message({
147
- message: "操作异常,请联系管理员",
175
+ message: this.$jx3boxT(
176
+ "jx3boxUi.wikiComment.actionUnavailable",
177
+ "操作异常,请联系管理员"
178
+ ),
148
179
  type: "warning",
149
180
  });
150
181
  return;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <WikiPanel class="c-wiki-comments" scene="detail">
2
+ <WikiPanel class="c-wiki-comments" scene="detail" :variant="variant">
3
3
  <template #head-title>
4
4
  <i class="u-icon el-icon-chat-line-round"></i>
5
5
  <span class="u-txt">{{ $jx3boxT('jx3boxUi.wikiComments.title', '百科评论') }}</span>
@@ -29,10 +29,18 @@
29
29
  <i class="el-icon-chat-dot-round"></i>
30
30
  <span>{{ $jx3boxT('jx3boxUi.wiki.reply', '回复') }}</span>
31
31
  </h4>
32
- <textarea class="u-reply-content" v-model="reply_form.content"></textarea>
32
+ <textarea
33
+ class="u-reply-content"
34
+ v-model="reply_form.content"
35
+ :aria-label="$jx3boxT('jx3boxUi.wiki.reply', '回复')"
36
+ ></textarea>
33
37
  <div class="u-author">
34
38
  <span>{{ $jx3boxT('jx3boxUi.wiki.nickname', '昵称:') }}</span>
35
- <input v-model="reply_form.user_nickname" type="text" />
39
+ <input
40
+ v-model="reply_form.user_nickname"
41
+ type="text"
42
+ :aria-label="$jx3boxT('jx3boxUi.wiki.nickname', '昵称:')"
43
+ />
36
44
  </div>
37
45
  <el-button type="primary" class="u-submit" @click="create_comment(reply_form)">
38
46
  <i class="el-icon-check"></i>
@@ -61,6 +69,11 @@ export default {
61
69
  type: [Number, String],
62
70
  default: 0,
63
71
  },
72
+ variant: {
73
+ type: String,
74
+ default: "default",
75
+ validator: (value) => ["default", "surface", "plain"].includes(value),
76
+ },
64
77
  },
65
78
  data() {
66
79
  return {
@@ -163,7 +176,9 @@ export default {
163
176
  });
164
177
  },
165
178
  getDefaultNickname() {
166
- return User.isLogin() ? User.getInfo().name : "神秘侠士";
179
+ return User.isLogin()
180
+ ? User.getInfo().name
181
+ : this.$jx3boxT("jx3boxUi.wiki.mysterious", "神秘侠士");
167
182
  },
168
183
  handleCurrentChange(page) {
169
184
  this.page = page;
@@ -1,9 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  class="c-wiki-panel"
4
- :class="{
5
- border: border,
6
- }"
4
+ :class="rootClasses"
7
5
  >
8
6
  <div class="m-panel-head">
9
7
  <slot name="head-before"></slot>
@@ -11,7 +9,13 @@
11
9
  <slot name="head-title"></slot>
12
10
  </div>
13
11
  <div class="m-panel-actions">
14
- <QRcode v-if="wikiPost && showQR" class="u-qr" />
12
+ <QRcode
13
+ v-if="wikiPost && showQR"
14
+ class="u-qr"
15
+ :trigger-text="$jx3boxT('jx3boxUi.wikiPanel.qrTrigger', '二维码')"
16
+ :popup-text="$jx3boxT('jx3boxUi.wikiPanel.qrScanToVisit', '扫一扫即可访问')"
17
+ popup-class="u-wiki-panel-qrcode-popup"
18
+ />
15
19
  <slot name="head-actions"></slot>
16
20
  </div>
17
21
  <slot name="head-after"></slot>
@@ -72,6 +76,11 @@ export default {
72
76
  type: String,
73
77
  default: "default",
74
78
  },
79
+ variant: {
80
+ type: String,
81
+ default: "default",
82
+ validator: (value) => ["default", "surface", "plain"].includes(value),
83
+ },
75
84
  border: {
76
85
  type: Boolean,
77
86
  default: true,
@@ -86,6 +95,15 @@ export default {
86
95
  stat: null,
87
96
  };
88
97
  },
98
+ computed: {
99
+ rootClasses() {
100
+ return {
101
+ border: this.border,
102
+ [`is-${this.variant}`]: true,
103
+ [`is-scene-${this.scene}`]: true,
104
+ };
105
+ },
106
+ },
89
107
  watch: {
90
108
  wikiPost: {
91
109
  immediate: true,
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <WikiPanel class="c-wiki-revisions" scene="detail">
2
+ <WikiPanel class="c-wiki-revisions" scene="detail" :variant="variant">
3
3
  <template #head-title>
4
4
  <i class="u-icon el-icon-time"></i>
5
5
  <span class="u-txt">{{ $jx3boxT('jx3boxUi.wikiRevisions.title', '历史版本') }}</span>
@@ -62,6 +62,11 @@ export default {
62
62
  type: [Boolean, Number],
63
63
  default: false,
64
64
  },
65
+ variant: {
66
+ type: String,
67
+ default: "default",
68
+ validator: (value) => ["default", "surface", "plain"].includes(value),
69
+ },
65
70
  },
66
71
  data: function () {
67
72
  return {
@@ -0,0 +1,113 @@
1
+ const assert = require("node:assert/strict");
2
+ const test = require("node:test");
3
+
4
+ require("@babel/register")({
5
+ babelrc: false,
6
+ configFile: false,
7
+ extensions: [".js"],
8
+ ignore: [/node_modules/],
9
+ plugins: ["@babel/plugin-transform-modules-commonjs"],
10
+ });
11
+
12
+ const {
13
+ createJx3boxUiI18n,
14
+ getJx3boxUiMessages,
15
+ mergeJx3boxUiMessages,
16
+ setJx3boxUiI18n,
17
+ } = require("../i18n");
18
+ const i18nMixin = require("../i18n/mixin").default;
19
+ const { createI18n } = require("vue-i18n");
20
+
21
+ const TARGET_PREFIXES = [
22
+ "wiki.",
23
+ "wikiPanel.",
24
+ "wikiComments.",
25
+ "wikiComment.",
26
+ "wikiRevisions.",
27
+ "gamePrice.",
28
+ ];
29
+
30
+ function flattenMessages(value, prefix = "", output = {}) {
31
+ Object.entries(value || {}).forEach(([key, child]) => {
32
+ const path = prefix ? `${prefix}.${key}` : key;
33
+ if (child && typeof child === "object" && !Array.isArray(child)) {
34
+ flattenMessages(child, path, output);
35
+ } else {
36
+ output[path] = child;
37
+ }
38
+ });
39
+ return output;
40
+ }
41
+
42
+ function placeholders(value) {
43
+ return [...String(value ?? "").matchAll(/\{(\w+)\}/g)].map((match) => match[1]).sort();
44
+ }
45
+
46
+ test("loads the target component messages for each supported locale", () => {
47
+ const messages = getJx3boxUiMessages();
48
+ Object.entries(messages).forEach(([locale, message]) => {
49
+ const flattened = flattenMessages(message);
50
+ TARGET_PREFIXES.forEach((prefix) => {
51
+ assert.ok(
52
+ Object.keys(flattened).some((key) => key.startsWith(prefix)),
53
+ `${locale} is missing ${prefix}`
54
+ );
55
+ });
56
+ });
57
+
58
+ const i18n = createJx3boxUiI18n({ locale: "en-US" });
59
+ assert.equal(i18n.global.t("jx3boxUi.wikiComment.setTop"), "Pin");
60
+ assert.equal(i18n.global.t("jx3boxUi.wikiPanel.qrTrigger"), "QR code");
61
+ assert.equal(i18n.global.t("jx3boxUi.wikiPanel.qrScanToVisit"), "Scan to open this page");
62
+ assert.equal(i18n.global.t("jx3boxUi.gamePrice.jin"), "gold");
63
+ });
64
+
65
+ test("merges target messages into a host i18n instance", () => {
66
+ const hostI18n = createI18n({
67
+ legacy: false,
68
+ locale: "en-US",
69
+ fallbackLocale: "zh-CN",
70
+ messages: {
71
+ "en-US": {
72
+ host: {
73
+ title: "Host",
74
+ },
75
+ },
76
+ },
77
+ });
78
+
79
+ mergeJx3boxUiMessages(hostI18n);
80
+ setJx3boxUiI18n(hostI18n);
81
+
82
+ assert.equal(hostI18n.global.t("host.title"), "Host");
83
+ assert.equal(hostI18n.global.t("jx3boxUi.wikiComments.title"), "Wiki Comments");
84
+ });
85
+
86
+ test("component mixin keeps the explicit fallback when the host misses a key", () => {
87
+ setJx3boxUiI18n(null);
88
+ const context = {
89
+ $t(key) {
90
+ return key;
91
+ },
92
+ };
93
+
94
+ assert.equal(
95
+ i18nMixin.methods.$jx3boxT.call(context, "jx3boxUi.missing.example", "欢迎 {name}", { name: "侠士" }),
96
+ "欢迎 侠士"
97
+ );
98
+ });
99
+
100
+ test("keeps four locale message keys and placeholders aligned", () => {
101
+ const messages = getJx3boxUiMessages();
102
+ const locales = Object.keys(messages);
103
+ const baseline = flattenMessages(messages["zh-CN"]);
104
+ const baselineKeys = Object.keys(baseline).sort();
105
+
106
+ locales.forEach((locale) => {
107
+ const current = flattenMessages(messages[locale]);
108
+ assert.deepEqual(Object.keys(current).sort(), baselineKeys, `${locale} message keys differ from zh-CN`);
109
+ baselineKeys.forEach((key) => {
110
+ assert.deepEqual(placeholders(current[key]), placeholders(baseline[key]), `${locale}:${key} placeholders differ`);
111
+ });
112
+ });
113
+ });