Skip to content

Commit 0bad5b1

Browse files
committed
add: localized command messages for German, English, and Vietnamese
- Created command_messages.yml for German (de_DE) with various command feedback messages. - Created command_messages.yml for English (en_US) with detailed command usage and feedback messages. - Created command_messages.yml for Vietnamese (vi_VN) with localized command feedback messages.
1 parent fe2d2a5 commit 0bad5b1

33 files changed

Lines changed: 1547 additions & 1165 deletions

core/build.gradle.kts

Lines changed: 163 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,17 @@ tasks.processResources {
129129
// ./gradlew generateLanguageChangelog
130130
//
131131
// It compares the current project version against the latest published GitHub
132-
// release. If the build is newer it prepends a new skeleton entry to
133-
// core/src/main/resources/language/CHANGELOG.txt – the human-readable file
134-
// that is extracted to plugins/SmartSpawner/language/CHANGELOG.txt on every
135-
// server start so admins know which language keys changed.
132+
// release. If the build is newer it fetches the old en_US language files from
133+
// GitHub, diffs their keys against the local ones, and prepends an entry to
134+
// core/src/main/resources/language/CHANGELOG.txt showing exactly which keys
135+
// were ADDED, CHANGED, or REMOVED.
136136
//
137137
// The task is skipped gracefully when GitHub is unreachable (offline / CI
138138
// without network access).
139139
// ─────────────────────────────────────────────────────────────────────────────
140140
tasks.register("generateLanguageChangelog") {
141141
group = "documentation"
142-
description = "Prepends a new skeleton entry to language/CHANGELOG.txt when the build version exceeds the latest GitHub release."
142+
description = "Diffs en_US language keys vs the latest GitHub release and prepends a changelog entry."
143143

144144
val changelogFile = project.file("src/main/resources/language/CHANGELOG.txt")
145145

@@ -148,6 +148,8 @@ tasks.register("generateLanguageChangelog") {
148148

149149
doLast {
150150
val currentVersion = project.version.toString()
151+
val locale = "en_US"
152+
val langFiles = listOf("messages.yml", "gui.yml", "items.yml", "formatting.yml", "command_messages.yml")
151153

152154
// ── 1. Fetch latest GitHub release tag ───────────────────────────────
153155
val githubVersion: String = try {
@@ -198,28 +200,172 @@ tasks.register("generateLanguageChangelog") {
198200
return@doLast
199201
}
200202

201-
// ── 4. Build plain-text entry (matches CHANGELOG.txt style) ──────────
202-
val today = LocalDate.now().toString()
203+
// ── 3b. Resolve the "to" ref: use the version tag if it exists, else main ──
204+
// Tags in this repo have no "v" prefix (e.g. "1.6.2"), so we check
205+
// both the bare version and the v-prefixed form.
206+
val toRef: String = run {
207+
listOf(currentVersion, "v$currentVersion").firstOrNull { tag ->
208+
try {
209+
val tagConn = URI.create(
210+
"https://api.github.com/repos/NighterDevelopment/SmartSpawner/git/refs/tags/$tag"
211+
).toURL().openConnection() as java.net.HttpURLConnection
212+
tagConn.requestMethod = "GET"
213+
tagConn.setRequestProperty("Accept", "application/vnd.github.v3+json")
214+
tagConn.setRequestProperty("User-Agent", "SmartSpawner-Changelog-Bot/1.0")
215+
tagConn.connectTimeout = 6_000
216+
tagConn.readTimeout = 6_000
217+
tagConn.responseCode == 200
218+
} catch (e: Exception) { false }
219+
} ?: "main"
220+
}
221+
println("[changelog] Compare range: $githubVersion...$toRef")
222+
223+
// ── 4. YAML flat-key extractor ───────────────────────────────────────
224+
// Returns map of dotPath → Pair(1-based lineNumber, rawValue)
225+
// Handles comments, blank lines, list items, and block scalars.
226+
fun extractKeys(yaml: String): LinkedHashMap<String, Pair<Int, String>> {
227+
val result = LinkedHashMap<String, Pair<Int, String>>()
228+
// stack entries: indent-level → key-name
229+
val stack = ArrayDeque<Pair<Int, String>>()
230+
var blockScalarIndent = -1 // >=0 while inside a | or > scalar
231+
232+
yaml.lines().forEachIndexed { idx, raw ->
233+
val lineNo = idx + 1
234+
val trimmed = raw.trimStart()
235+
if (trimmed.isEmpty() || trimmed.startsWith('#')) return@forEachIndexed
236+
237+
val indent = raw.length - trimmed.length
238+
239+
// Leaving a block scalar when indentation returns to its level
240+
if (blockScalarIndent >= 0) {
241+
if (indent > blockScalarIndent) return@forEachIndexed
242+
else blockScalarIndent = -1
243+
}
244+
245+
// List items are not individually tracked as keys
246+
if (trimmed.startsWith("- ") || trimmed == "-") return@forEachIndexed
247+
248+
val colonIdx = trimmed.indexOf(':')
249+
if (colonIdx < 0) return@forEachIndexed
250+
251+
val key = trimmed.substring(0, colonIdx).trim()
252+
if (key.isEmpty() || key.startsWith('#')) return@forEachIndexed
253+
254+
var value = trimmed.substring(colonIdx + 1).trim()
255+
256+
// Strip trailing inline comment (only outside quoted values)
257+
if (!value.startsWith('"') && !value.startsWith('\'')) {
258+
val ci = value.indexOf(" #")
259+
if (ci >= 0) value = value.substring(0, ci).trim()
260+
}
261+
262+
// Pop stack entries whose indent >= current level
263+
while (stack.isNotEmpty() && stack.last().first >= indent) stack.removeLast()
264+
265+
val path = if (stack.isEmpty()) key
266+
else stack.joinToString(".") { it.second } + ".$key"
267+
268+
if (value == "|" || value == ">" || value == "|-" || value == ">-"
269+
|| value == "|+" || value == ">+") {
270+
result[path] = lineNo to value
271+
blockScalarIndent = indent
272+
} else {
273+
result[path] = lineNo to value
274+
}
275+
276+
stack.addLast(indent to key)
277+
}
278+
return result
279+
}
280+
281+
// Truncate long values for display
282+
fun truncate(s: String, max: Int = 70) = if (s.length > max) s.take(max) + "" else s
283+
284+
// ── 5. Fetch the old language files from GitHub and diff ─────────────
285+
fun fetchRaw(tag: String, file: String): String? = try {
286+
val url = "https://raw.githubusercontent.com/NighterDevelopment/" +
287+
"SmartSpawner/$tag/core/src/main/resources/language/$locale/$file"
288+
val conn = URI.create(url).toURL().openConnection() as java.net.HttpURLConnection
289+
conn.requestMethod = "GET"
290+
conn.setRequestProperty("User-Agent", "SmartSpawner-Changelog-Bot/1.0")
291+
conn.connectTimeout = 8_000
292+
conn.readTimeout = 8_000
293+
if (conn.responseCode == 200) conn.inputStream.bufferedReader().readText() else null
294+
} catch (e: Exception) { null }
295+
296+
// Per-file diffs: maps file name → pre-formatted entry lines
297+
val addedPerFile = mutableMapOf<String, List<String>>()
298+
val changedPerFile = mutableMapOf<String, List<String>>()
299+
val removedPerFile = mutableMapOf<String, List<String>>()
300+
301+
for (file in langFiles) {
302+
val oldYaml = fetchRaw(githubVersion, file)
303+
if (oldYaml == null) {
304+
println("[changelog] ⚠ Could not fetch $file @ $githubVersion – skipping diff.")
305+
continue
306+
}
307+
val newFile = project.file("src/main/resources/language/$locale/$file")
308+
if (!newFile.exists()) continue
309+
310+
val oldKeys = extractKeys(oldYaml)
311+
val newKeys = extractKeys(newFile.readText())
312+
313+
val added = newKeys.entries
314+
.filter { it.key !in oldKeys }
315+
.sortedBy { it.key }
316+
.map { (k, e) -> " - $k (L${e.first}): ${truncate(e.second)}" }
317+
318+
val removed = oldKeys.entries
319+
.filter { it.key !in newKeys }
320+
.sortedBy { it.key }
321+
.map { (k, e) -> " - $k (L${e.first}): ${truncate(e.second)}" }
322+
323+
val changed = newKeys.entries
324+
.filter { it.key in oldKeys && oldKeys[it.key]!!.second != it.value.second }
325+
.sortedBy { it.key }
326+
.map { (k, newE) ->
327+
val oldVal = truncate(oldKeys[k]!!.second)
328+
val newVal = truncate(newE.second)
329+
" - $k (L${newE.first}): $oldVal$newVal"
330+
}
331+
332+
if (added.isNotEmpty()) addedPerFile[file] = added
333+
if (changed.isNotEmpty()) changedPerFile[file] = changed
334+
if (removed.isNotEmpty()) removedPerFile[file] = removed
335+
336+
println("[changelog] $file – +${added.size} ~${changed.size} -${removed.size}")
337+
}
338+
339+
// ── 6. Build the CHANGELOG entry ─────────────────────────────────────
340+
// perFile values are already fully-formatted lines (" - key (Lnn): value")
341+
fun formatSection(label: String, perFile: Map<String, List<String>>): String {
342+
if (perFile.isEmpty()) return " $label:\n (none)"
343+
val sb = StringBuilder(" $label:\n")
344+
for ((file, lines) in perFile) {
345+
sb.appendLine(" $file:")
346+
lines.forEach { sb.appendLine(it) }
347+
}
348+
return sb.toString().trimEnd()
349+
}
350+
351+
val today = LocalDate.now().toString()
203352
val separator = "".repeat(80 - "── v$currentVersion ($today) ".length)
204-
val newEntry = buildString {
353+
val newEntry = buildString {
205354
appendLine("── v$currentVersion ($today) $separator")
206355
appendLine()
207356
appendLine(" Summary: Version $currentVersion released – fill in details here.")
208-
appendLine(" Compare: https://github.com/NighterDevelopment/SmartSpawner/compare/v$githubVersion...v$currentVersion")
357+
appendLine(" Compare: https://github.com/NighterDevelopment/SmartSpawner/compare/$githubVersion...$toRef")
209358
appendLine()
210-
appendLine(" ADDED:")
211-
appendLine(" (none)")
359+
appendLine(formatSection("ADDED", addedPerFile))
212360
appendLine()
213-
appendLine(" CHANGED:")
214-
appendLine(" (none)")
361+
appendLine(formatSection("CHANGED", changedPerFile))
215362
appendLine()
216-
appendLine(" REMOVED:")
217-
appendLine(" (none)")
363+
appendLine(formatSection("REMOVED", removedPerFile))
218364
appendLine()
219365
}
220366

221-
// ── 5. Insert before the first existing version entry ────────────────
222-
val marker = "\n──"
367+
// ── 7. Insert before the first existing version entry ────────────────
368+
val marker = "\n──"
223369
val updated = if (existing.contains(marker)) {
224370
existing.replaceFirst(marker, "\n$newEntry──")
225371
} else {

core/src/main/java/github/nighter/smartspawner/SmartSpawner.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import github.nighter.smartspawner.bstats.Metrics;
55
import github.nighter.smartspawner.commands.BrigadierCommandManager;
66
import github.nighter.smartspawner.commands.list.ListSubCommand;
7+
import github.nighter.smartspawner.commands.near.NearResultGUI;
78
import github.nighter.smartspawner.commands.near.SpawnerHighlightManager;
89
import github.nighter.smartspawner.commands.list.gui.list.UserPreferenceCache;
910
import github.nighter.smartspawner.commands.list.gui.list.SpawnerListGUI;
@@ -156,6 +157,7 @@ public class SmartSpawner extends JavaPlugin implements SmartSpawnerPlugin {
156157

157158
// Near-command highlight manager
158159
private SpawnerHighlightManager spawnerHighlightManager;
160+
private NearResultGUI nearResultGUI;
159161

160162
// API implementation
161163
private SmartSpawnerAPIImpl apiImpl;
@@ -409,6 +411,7 @@ private void initializeHandlers() {
409411
this.spawnerStackHandler = new SpawnerStackHandler(this);
410412
this.spawnerClickManager = new SpawnerClickManager(this);
411413
this.spawnerHighlightManager = new SpawnerHighlightManager(this);
414+
this.nearResultGUI = new NearResultGUI(this, spawnerHighlightManager);
412415
}
413416

414417
private void initializeUIAndActions() {
@@ -464,6 +467,9 @@ private void registerListeners() {
464467
if (spawnerHighlightManager != null) {
465468
pm.registerEvents(spawnerHighlightManager, this);
466469
}
470+
if (nearResultGUI != null) {
471+
pm.registerEvents(nearResultGUI, this);
472+
}
467473

468474
// Register logging listener
469475
if (spawnerAuditListener != null) {

core/src/main/java/github/nighter/smartspawner/commands/clear/ClearGhostSpawnersSubCommand.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public int execute(CommandContext<CommandSourceStack> context) {
4040
CommandSender sender = context.getSource().getSender();
4141

4242
// Notify that the check is starting
43-
plugin.getMessageService().sendMessage(sender, "command_ghost_spawner_check_start");
43+
plugin.getMessageService().sendMessage(sender, "clear.ghost_check_start");
4444

4545
// Get all spawners first
4646
List<SpawnerData> allSpawners = plugin.getSpawnerManager().getAllSpawners();
@@ -66,10 +66,10 @@ public int execute(CommandContext<CommandSourceStack> context) {
6666
Scheduler.runTaskLater(() -> {
6767
int count = removedCount.get();
6868
if (count > 0) {
69-
plugin.getMessageService().sendMessage(sender, "command_ghost_spawner_cleared",
69+
plugin.getMessageService().sendMessage(sender, "clear.ghost_cleared",
7070
java.util.Map.of("count", String.valueOf(count)));
7171
} else {
72-
plugin.getMessageService().sendMessage(sender, "command_ghost_spawner_none_found");
72+
plugin.getMessageService().sendMessage(sender, "clear.ghost_none_found");
7373
}
7474
}, 100L); // Wait 5 seconds (100 ticks) for checks to complete
7575

core/src/main/java/github/nighter/smartspawner/commands/clear/ClearHologramsSubCommand.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ public int execute(CommandContext<CommandSourceStack> context) {
3939
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "minecraft:kill @e[type=text_display]");
4040

4141
// Send success message to player
42-
plugin.getMessageService().sendMessage(sender, "command_hologram_cleared");
42+
plugin.getMessageService().sendMessage(sender, "hologram.cleared");
4343

4444
return 1;
4545
} catch (Exception e) {
4646
plugin.getLogger().severe("Error clearing holograms: " + e.getMessage());
47-
plugin.getMessageService().sendMessage(sender, "command_hologram_clear_error");
47+
plugin.getMessageService().sendMessage(sender, "hologram.clear_error");
4848
return 0;
4949
}
5050
}

core/src/main/java/github/nighter/smartspawner/commands/clear/ClearSubCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public int execute(CommandContext<CommandSourceStack> context) {
5252
CommandSender sender = context.getSource().getSender();
5353

5454
// When no subcommand is provided, show usage
55-
plugin.getMessageService().sendMessage(sender, "clear_command_usage");
55+
plugin.getMessageService().sendMessage(sender, "clear.usage");
5656
return 0;
5757
}
5858
}

core/src/main/java/github/nighter/smartspawner/commands/give/GiveSubCommand.java

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ private int executeGive(CommandContext<CommandSourceStack> context, boolean isVa
140140
List<Player> players = playerSelector.resolve(context.getSource());
141141

142142
if (players.isEmpty()) {
143-
plugin.getMessageService().sendMessage(sender, "command_give_player_not_found");
143+
plugin.getMessageService().sendMessage(sender, "give.player_not_found");
144144
return 0;
145145
}
146146

@@ -149,7 +149,7 @@ private int executeGive(CommandContext<CommandSourceStack> context, boolean isVa
149149

150150
// Validate mob type (case insensitive check)
151151
if (!supportedMobs.contains(mobType.toUpperCase())) {
152-
plugin.getMessageService().sendMessage(sender, "command_give_invalid_mob_type");
152+
plugin.getMessageService().sendMessage(sender, "give.invalid_mob_type");
153153
return 0;
154154
}
155155

@@ -167,7 +167,7 @@ private int executeGive(CommandContext<CommandSourceStack> context, boolean isVa
167167
// Give the item to the player
168168
if (target.getInventory().firstEmpty() == -1) {
169169
target.getWorld().dropItem(target.getLocation(), spawnerItem);
170-
plugin.getMessageService().sendMessage(target, "command_give_inventory_full");
170+
plugin.getMessageService().sendMessage(target, "give.inventory_full");
171171
} else {
172172
target.getInventory().addItem(spawnerItem);
173173
}
@@ -193,9 +193,8 @@ private int executeGive(CommandContext<CommandSourceStack> context, boolean isVa
193193
targetPlaceholders.put("ᴇɴᴛɪᴛʏ", smallCapsEntityName);
194194

195195
// Send messages with placeholders
196-
String messageKey = "command_give_spawner_";
197-
plugin.getMessageService().sendMessage(sender, messageKey + "given", senderPlaceholders);
198-
plugin.getMessageService().sendMessage(target, messageKey + "received", targetPlaceholders);
196+
plugin.getMessageService().sendMessage(sender, "give.spawner_given", senderPlaceholders);
197+
plugin.getMessageService().sendMessage(target, "give.spawner_received", targetPlaceholders);
199198

200199
return 1;
201200
} catch (Exception e) {
@@ -216,7 +215,7 @@ private int executeGiveItemSpawner(CommandContext<CommandSourceStack> context, i
216215
List<Player> players = playerSelector.resolve(context.getSource());
217216

218217
if (players.isEmpty()) {
219-
plugin.getMessageService().sendMessage(sender, "command_give_player_not_found");
218+
plugin.getMessageService().sendMessage(sender, "give.player_not_found");
220219
return 0;
221220
}
222221

@@ -228,13 +227,13 @@ private int executeGiveItemSpawner(CommandContext<CommandSourceStack> context, i
228227
try {
229228
itemMaterial = Material.valueOf(itemType.toUpperCase());
230229
} catch (IllegalArgumentException e) {
231-
plugin.getMessageService().sendMessage(sender, "command_give_invalid_item_type");
230+
plugin.getMessageService().sendMessage(sender, "give.invalid_item_type");
232231
return 0;
233232
}
234233

235234
// Verify it's a valid item spawner type
236235
if (!plugin.getItemSpawnerSettingsConfig().isValidItemSpawner(itemMaterial)) {
237-
plugin.getMessageService().sendMessage(sender, "command_give_invalid_item_spawner");
236+
plugin.getMessageService().sendMessage(sender, "give.invalid_item_spawner");
238237
return 0;
239238
}
240239

@@ -244,7 +243,7 @@ private int executeGiveItemSpawner(CommandContext<CommandSourceStack> context, i
244243
// Give the item to the player
245244
if (target.getInventory().firstEmpty() == -1) {
246245
target.getWorld().dropItem(target.getLocation(), spawnerItem);
247-
plugin.getMessageService().sendMessage(target, "command_give_inventory_full");
246+
plugin.getMessageService().sendMessage(target, "give.inventory_full");
248247
} else {
249248
target.getInventory().addItem(spawnerItem);
250249
}
@@ -270,9 +269,8 @@ private int executeGiveItemSpawner(CommandContext<CommandSourceStack> context, i
270269
targetPlaceholders.put("ᴇɴᴛɪᴛʏ", smallCapsItemName);
271270

272271
// Send messages with placeholders (use same keys as regular spawners)
273-
String messageKey = "command_give_spawner_";
274-
plugin.getMessageService().sendMessage(sender, messageKey + "given", senderPlaceholders);
275-
plugin.getMessageService().sendMessage(target, messageKey + "received", targetPlaceholders);
272+
plugin.getMessageService().sendMessage(sender, "give.spawner_given", senderPlaceholders);
273+
plugin.getMessageService().sendMessage(target, "give.spawner_received", targetPlaceholders);
276274

277275
return 1;
278276
} catch (Exception e) {

0 commit comments

Comments
 (0)