-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcommandLocalization.js
More file actions
178 lines (164 loc) · 8.74 KB
/
Copy pathcommandLocalization.js
File metadata and controls
178 lines (164 loc) · 8.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
'use strict';
const ZH_TW = 'zh-TW';
const CJK = /[\u3400-\u9fff]/u;
const NAME_OVERRIDES = Object.freeze({
'anonavatar 風格': 'style',
'anonavatar 自訂圖': 'upload',
'anonimage 圖片': 'image',
'anonimage 文字': 'caption',
'匿名回覆': 'Anonymous reply',
'回報匿名訊息': 'Report anonymous message',
'追溯作者': 'Reveal author',
});
const DESCRIPTION_OVERRIDES = Object.freeze({
setup: 'Post the member verification panel (administrators only).',
adminpanel: 'Create a one-time secure admin link (administrators only).',
anonadmin: 'Audit and manage pseudonymous messages (administrators only).',
anonprofile: 'View your pseudonym profile and privacy information.',
anonname: 'Change your pseudonym.',
anonavatar: 'Change your pseudonym avatar or upload a reviewed image.',
anonimage: 'Post a reviewed image under your pseudonym in this channel.',
anonprivacy: 'Read this server’s pseudonym privacy and identity-reveal policy.',
anonchannel: 'Configure server-wide immersive pseudonymity (administrators only).',
nomention: 'Block member mentions in selected channels (administrators only).',
autoclear: 'Schedule message clearing for selected channels (administrators only).',
appeal: 'Ask moderators to review your latest rejected pseudonymous message.',
help: 'Ask the AI support assistant how to use this community.',
rules: 'Read the community rules and pseudonym safety notes.',
stats: 'Manage private aggregate server counters (administrators only).',
ticket: 'Configure and manage private support tickets (administrators only).',
'匿名回覆': 'Reply under your pseudonym.',
'回報匿名訊息': 'Report a pseudonymous message for human review.',
'追溯作者': 'Reveal the author with an audit record.',
'anonadmin reveal': 'Reveal the author of one pseudonymous message.',
'anonadmin whois': 'Find a member from a visible pseudonym.',
'anonadmin history': 'Review a member’s pseudonymous message history.',
'anonadmin block': 'Disable pseudonymous posting for a member.',
'anonadmin unblock': 'Restore pseudonymous posting for a member.',
'anonadmin blocklist': 'View members blocked from pseudonymous posting.',
'anonadmin off': 'Disable pseudonymous posting in a channel.',
'anonadmin system': 'Enable or disable the pseudonym system globally.',
'anonadmin resetname': 'Retire a member’s current pseudonym.',
'anonadmin audit': 'View identity-reveal and moderation audit records.',
'anonchannel enable': 'Enable immersive pseudonymity across the server.',
'anonchannel disable': 'Disable immersive pseudonymity and remove its AutoMod rule.',
'anonchannel exempt': 'Add or remove a channel or category exemption.',
'anonchannel status': 'View immersive mode, exemptions, and permission checks.',
'anonchannel decoy': 'Configure optional decoy typing indicators.',
'nomention add': 'Add a channel or category to the no-mention list.',
'nomention remove': 'Remove a channel or category from the no-mention list.',
'nomention list': 'View no-mention rules and deletion permissions.',
'autoclear set': 'Create or update a clearing schedule without clearing now.',
'autoclear off': 'Stop a channel’s clearing schedule.',
'autoclear status': 'View schedules and their latest results.',
'stats add': 'Add one aggregate counter channel.',
'stats remove': 'Remove an aggregate counter and its channel.',
'stats list': 'View counters, update times, and permission checks.',
'stats refresh': 'Request an update while respecting Discord rename limits.',
'stats history': 'View the last 14 days of aggregate daily snapshots.',
'ticket panel': 'Post the open-ticket panel in this channel.',
'ticket staff': 'Manage staff roles that can view and claim tickets.',
'ticket logchannel': 'Set or view the transcript notification channel.',
'ticket lifecycle': 'Configure automatic closing, deletion, and reminders.',
'ticket settings': 'View ticket settings and permission checks.',
'ticket add': 'Add a member to the current ticket.',
'ticket remove': 'Remove a member from the current ticket.',
'ticket close': 'Close the current ticket.',
'ticket transcript': 'Create a fresh transcript link for this ticket.',
'ticket note': 'Add a staff-only note to the current ticket.',
'ticket notes': 'View staff-only notes for the current ticket.',
'ticket aihint': 'Configure the pre-ticket AI support suggestion.',
'ticket type': 'Manage ticket types shown in the panel.',
'ticket type add': 'Add or update a ticket type.',
'ticket type remove': 'Remove a ticket type.',
'ticket type list': 'List all ticket types.',
'ticket field': 'Manage up to five intake fields for each ticket type.',
'ticket field add': 'Add an intake field to a ticket type.',
'ticket field remove': 'Remove an intake field by position.',
'ticket field list': 'List the intake fields for a ticket type.',
'ticket blacklist': 'Manage members who cannot open tickets.',
'ticket blacklist add': 'Prevent a member from opening tickets.',
'ticket blacklist remove': 'Allow a member to open tickets again.',
'ticket blacklist list': 'List members blocked from opening tickets.',
});
const GENERIC_DESCRIPTIONS = Object.freeze({
action: 'Choose an action.',
alias: 'Enter the visible pseudonym.',
autoclose_days: 'Close inactive open tickets after this many days (0 disables).',
autodelete_days: 'Delete closed ticket channels after this many days (0 disables).',
caption: 'Optional image caption or same-channel message link.',
channel: 'Select a channel.',
clear: 'Clear this configuration and restore defaults.',
description: 'Optional description.',
hours: 'Number of hours between runs.',
id: 'Enter the numeric channel ID.',
image: 'Choose the image to post.',
index: 'Enter the field position shown by the list command.',
key: 'Enter the short type key.',
label: 'Enter the user-facing label.',
long: 'Use a multi-line input.',
name: 'Enter the new pseudonym (2–20 characters).',
question: 'Enter your question.',
reason: 'Optional reason for the audit record.',
reminder_hours: 'Remind staff after this many hours without a reply (0 disables).',
required: 'Require this field.',
role: 'Select a role.',
state: 'Choose on or off.',
style: 'Choose an avatar style, or keep the current style.',
target: 'Select or enter the target.',
template: 'Optional name template; use {count} for the value.',
text: 'Enter the text.',
title: 'Optional title.',
type: 'Choose or enter the type.',
upload: 'Upload a reviewed custom avatar image.',
user: 'Select a member.',
});
function englishFirstCommands(commands) {
if (!Array.isArray(commands)) throw new TypeError('commands must be an array');
return commands.map(command => localizeNode(command, []));
}
function localizeNode(input, parentPath) {
const node = { ...input };
const originalName = String(node.name || '');
const originalPath = [...parentPath, originalName].filter(Boolean);
const key = originalPath.join(' ');
const canonicalName = NAME_OVERRIDES[key] || originalName;
if (canonicalName !== originalName) {
node.name = canonicalName;
node.name_localizations = { ...(node.name_localizations || {}), [ZH_TW]: originalName };
}
if (typeof node.description === 'string' && node.description) {
const originalDescription = node.description;
node.description = DESCRIPTION_OVERRIDES[key]
|| GENERIC_DESCRIPTIONS[canonicalName]
|| `Configure ${humanize(canonicalName)}.`;
if (CJK.test(originalDescription)) {
node.description_localizations = {
...(node.description_localizations || {}),
[ZH_TW]: originalDescription,
};
}
}
if (Array.isArray(node.choices)) {
node.choices = node.choices.map(choice => {
if (!choice || typeof choice.name !== 'string' || !CJK.test(choice.name)) return choice;
return {
...choice,
name: humanize(String(choice.value || 'option')),
name_localizations: { ...(choice.name_localizations || {}), [ZH_TW]: choice.name },
};
});
}
if (Array.isArray(node.options)) {
node.options = node.options.map(option => localizeNode(option, originalPath));
}
return node;
}
function humanize(value) {
const result = String(value || 'option')
.replace(/[_-]+/gu, ' ')
.replace(/\s+/gu, ' ')
.trim();
return result ? result.charAt(0).toUpperCase() + result.slice(1) : 'Option';
}
module.exports = { englishFirstCommands, ZH_TW };