使用宏自动从英文文档复制部分内容 (#4406)

This commit is contained in:
Glavo 2025-09-08 20:40:23 +08:00 committed by GitHub
parent 24fd2cf521
commit 480f8b6890
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 316 additions and 111 deletions

View File

@ -97,38 +97,46 @@ public record Document(DocumentFileTree directory,
String macroName = matcher.group("name");
String endLine = "<!-- #END " + macroName + " -->";
var properties = new LinkedHashMap<String, List<String>>();
var lines = new ArrayList<String>();
while (true) {
line = reader.readLine();
if (line == null)
throw new IOException("Missing end line for macro: " + macroName);
else if (line.startsWith("<!-- #END")) {
if (line.equals(endLine)) {
break;
} else {
throw new IOException("Invalid macro end line: " + line);
}
} else {
lines.add(line);
}
}
var properties = new LinkedHashMap<String, List<String>>();
int propertiesCount = 0;
// Handle properties
while ((line = reader.readLine()) != null) {
if (!line.startsWith("<!-- #") || line.equals(endLine))
for (String macroBodyLine : lines) {
if (!macroBodyLine.startsWith("<!-- #"))
break;
Matcher propertyMatcher = MACRO_PROPERTY_LINE.matcher(line);
Matcher propertyMatcher = MACRO_PROPERTY_LINE.matcher(macroBodyLine);
if (propertyMatcher.matches()) {
String propertyName = propertyMatcher.group("name");
String propertyValue = parsePropertyValue(propertyMatcher.group("value"));
properties.computeIfAbsent(propertyName, k -> new ArrayList<>(1))
.add(propertyValue);
propertiesCount++;
} else {
throw new IOException("Invalid macro property line: " + line);
throw new IOException("Invalid macro property line: " + macroBodyLine);
}
}
// Handle lines
if (line != null && !line.equals(endLine)) {
while ((line = reader.readLine()) != null) {
if (line.startsWith("<!-- #"))
break;
lines.add(line);
}
}
if (line == null || !line.equals(endLine))
throw new IOException("Invalid macro end line: " + line);
if (propertiesCount > 0)
lines.subList(0, propertiesCount).clear();
items.add(new MacroBlock(macroName,
Collections.unmodifiableMap(properties),

View File

@ -17,14 +17,64 @@
*/
package org.jackhuang.hmcl.gradle.docs;
import javax.print.Doc;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.*;
import java.util.regex.Pattern;
/// Macro processor for automatically updating documentation.
///
/// Users can use the macro processor in `.md` documents within the `docs` folder and its subfolders.
/// The parts to be processed should be wrapped with `<!-- #BEGIN MACRO_NAME -->` and `<!-- #END MACRO_NAME -->` lines.
///
/// For example, if you create a document `FOO.md` and translate it into Simplified Chinese, Traditional Chinese, and Japanese,
/// you can add the following content in these files to create links to other language versions:
///
/// ```markdown
/// <!-- #BEGIN LANGUAGE_SWITCHER -->
/// <!-- #END LANGUAGE_SWITCHER -->
/// ```
///
/// After running `./gradlew updateDocuments`, the macro processor will automatically update the content between these two lines:
///
/// ```
/// <!-- #BEGIN LANGUAGE_SWITCHER -->
/// **English** | 中文 ([简体](FOO_zh.md), [繁體](FOO_zh_Hant.md)) | [日本語](FOO_ja.md)
/// <!-- #END LANGUAGE_SWITCHER -->
/// ```
///
/// @author Glavo
public enum MacroProcessor {
/// Does not process the content in any way.
///
/// Supported properties:
///
/// - `NAME`: The name of this block (used by other macros).
/// - `PROCESS_LINK`: If set to `FALSE`, document links in the content will not be automatically updated.
BLOCK {
@Override
public void apply(Document document, Document.MacroBlock macroBlock, StringBuilder outputBuilder) throws IOException {
var mutableProperties = new LinkedHashMap<>(macroBlock.properties());
MacroProcessor.removeSingleProperty(mutableProperties, "NAME");
boolean processLink = !"FALSE".equalsIgnoreCase(MacroProcessor.removeSingleProperty(mutableProperties, "PROCESS_LINK"));
if (!mutableProperties.isEmpty())
throw new IllegalArgumentException("Unsupported properties: " + mutableProperties.keySet());
MacroProcessor.writeBegin(outputBuilder, macroBlock);
MacroProcessor.writeProperties(outputBuilder, macroBlock);
for (String line : macroBlock.contentLines()) {
if (processLink)
MacroProcessor.processLine(outputBuilder, line, document);
else
outputBuilder.append(line).append('\n');
}
MacroProcessor.writeEnd(outputBuilder, macroBlock);
}
},
/// Used to automatically generate links to other language versions of the current document.
///
/// Does not support any properties.
LANGUAGE_SWITCHER {
private static <T> boolean containsIdentity(List<T> list, T element) {
for (T t : list) {
@ -104,17 +154,187 @@ public enum MacroProcessor {
MacroProcessor.writeEnd(outputBuilder, macroBlock);
}
},
BLOCK {
/// Copy the block with the specified name from the English version of the current document.
///
/// Supported properties:
///
/// - `NAME` (required): Specifies the block to be copied.
/// - `REPLACE` (repeatable): Used to replace specified text. Accepts a list containing two strings. The first string is a regular expression for matching content; the second string is the replacement target.
/// - `PROCESS_LINK`: If set to `FALSE`, document links in the content will not be automatically updated.
COPY {
private record Replace(Pattern pattern, String replacement) {
}
private static IllegalArgumentException illegalReplace(String value) {
return new IllegalArgumentException("Illegal replacement pattern: " + value);
}
private static Replace parseReplace(String value) {
List<String> list = MacroProcessor.parseStringList(value);
if (list.size() != 2)
throw illegalReplace(value);
return new Replace(Pattern.compile(list.get(0)), list.get(1));
}
@Override
public void apply(Document document, Document.MacroBlock macroBlock, StringBuilder outputBuilder) throws IOException {
var mutableProperties = new LinkedHashMap<>(macroBlock.properties());
String blockName = MacroProcessor.removeSingleProperty(mutableProperties, "NAME");
if (blockName == null)
throw new IllegalArgumentException("Missing property: NAME");
List<Replace> replaces = Objects.requireNonNullElse(mutableProperties.remove("REPLACE"), List.<String>of())
.stream()
.map(it -> parseReplace(it))
.toList();
boolean processLink = !"FALSE".equalsIgnoreCase(MacroProcessor.removeSingleProperty(mutableProperties, "PROCESS_LINK"));
if (!mutableProperties.isEmpty())
throw new IllegalArgumentException("Unsupported properties: " + mutableProperties.keySet());
LocalizedDocument localizedDocument = document.directory().getFiles().get(document.name());
Document fromDocument;
if (localizedDocument == null || (fromDocument = localizedDocument.getDocuments().get(DocumentLocale.ENGLISH)) == null)
throw new IOException("Document " + document.name() + " for english does not exist");
List<String> nameList = List.of(blockName);
var fromBlock = (Document.MacroBlock) fromDocument.items().stream()
.filter(it -> it instanceof Document.MacroBlock macro
&& macro.name().equals(BLOCK.name())
&& nameList.equals(macro.properties().get("NAME"))
)
.findFirst()
.orElseThrow(() -> new IOException("Cannot find the block \"" + blockName + "\" in " + fromDocument.file()));
MacroProcessor.writeBegin(outputBuilder, macroBlock);
MacroProcessor.writeProperties(outputBuilder, macroBlock);
for (String line : macroBlock.contentLines()) {
outputBuilder.append(line).append('\n');
for (String line : fromBlock.contentLines()) {
for (Replace replace : replaces) {
line = replace.pattern.matcher(line).replaceAll(replace.replacement());
}
if (processLink)
processLine(outputBuilder, line, document);
else
outputBuilder.append(line).append('\n');
}
MacroProcessor.writeEnd(outputBuilder, macroBlock);
}
};
},
;
private static String removeSingleProperty(Map<String, List<String>> properties, String name) {
List<String> values = properties.remove(name);
if (values == null || values.isEmpty())
return null;
if (values.size() != 1)
throw new IllegalArgumentException("Unexpected number of property " + name + ": " + values.size());
return values.get(0);
}
private static List<String> parseStringList(String str) {
if (str.isBlank()) {
return new ArrayList<>();
}
// Split the string with ' and space cleverly.
ArrayList<String> parts = new ArrayList<>(2);
boolean hasValue = false;
StringBuilder current = new StringBuilder(str.length());
for (int i = 0; i < str.length(); ) {
char c = str.charAt(i);
if (c == '\'' || c == '"') {
hasValue = true;
int end = str.indexOf(c, i + 1);
if (end < 0) {
end = str.length();
}
current.append(str, i + 1, end);
i = end + 1;
} else if (c == ' ' || c == '\t') {
if (hasValue) {
parts.add(current.toString());
current.setLength(0);
hasValue = false;
}
i++;
} else {
hasValue = true;
current.append(c);
i++;
}
}
if (hasValue)
parts.add(current.toString());
return parts;
}
private static final Pattern LINK_PATTERN = Pattern.compile(
"(?<=]\\()[a-zA-Z0-9_\\-./]+\\.md(?=\\))"
);
static void processLine(StringBuilder outputBuilder, String line, Document document) {
outputBuilder.append(LINK_PATTERN.matcher(line).replaceAll(matchResult -> {
String rawLink = matchResult.group();
String[] splitPath = rawLink.split("/");
if (splitPath.length == 0)
return rawLink;
String fileName = splitPath[splitPath.length - 1];
if (!fileName.endsWith(".md"))
return rawLink;
DocumentFileTree current = document.directory();
for (int i = 0; i < splitPath.length - 1; i++) {
String name = splitPath[i];
switch (name) {
case "" -> {
return rawLink;
}
case "." -> {
continue;
}
case ".." -> {
current = current.getParent();
if (current == null)
return rawLink;
}
default -> {
current = current.getChildren().get(name);
if (current == null)
return rawLink;
}
}
}
DocumentLocale.LocaleAndName currentLocaleAndName = DocumentLocale.parseFileName(fileName.substring(0, fileName.length() - ".md".length()));
LocalizedDocument localizedDocument = current.getFiles().get(currentLocaleAndName.name());
if (localizedDocument != null) {
List<DocumentLocale> candidateLocales = document.locale().getCandidates();
for (DocumentLocale candidateLocale : candidateLocales) {
if (candidateLocale == currentLocaleAndName.locale())
return rawLink;
Document targetDoc = localizedDocument.getDocuments().get(candidateLocale);
if (targetDoc != null) {
splitPath[splitPath.length - 1] = targetDoc.file().getFileName().toString();
return String.join("/", splitPath);
}
}
}
return rawLink;
})).append('\n');
}
private static void writeBegin(StringBuilder builder, Document.MacroBlock macroBlock) throws IOException {
builder.append("<!-- #BEGIN ");

View File

@ -25,8 +25,6 @@ import org.gradle.api.tasks.TaskAction;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
/// @author Glavo
public abstract class UpdateDocuments extends DefaultTask {
@ -36,71 +34,12 @@ public abstract class UpdateDocuments extends DefaultTask {
// ---
private static final Pattern LINK_PATTERN = Pattern.compile(
"(?<=]\\()[a-zA-Z0-9_\\-./]+\\.md(?=\\))"
);
private void processLine(StringBuilder outputBuilder, String line, Document document) {
outputBuilder.append(LINK_PATTERN.matcher(line).replaceAll(matchResult -> {
String rawLink = matchResult.group();
String[] splitPath = rawLink.split("/");
if (splitPath.length == 0)
return rawLink;
String fileName = splitPath[splitPath.length - 1];
if (!fileName.endsWith(".md"))
return rawLink;
DocumentFileTree current = document.directory();
for (int i = 0; i < splitPath.length - 1; i++) {
String name = splitPath[i];
switch (name) {
case "" -> {
return rawLink;
}
case "." -> {
continue;
}
case ".." -> {
current = current.getParent();
if (current == null)
return rawLink;
}
default -> {
current = current.getChildren().get(name);
if (current == null)
return rawLink;
}
}
}
DocumentLocale.LocaleAndName currentLocaleAndName = DocumentLocale.parseFileName(fileName.substring(0, fileName.length() - ".md".length()));
LocalizedDocument localizedDocument = current.getFiles().get(currentLocaleAndName.name());
if (localizedDocument != null) {
List<DocumentLocale> candidateLocales = document.locale().getCandidates();
for (DocumentLocale candidateLocale : candidateLocales) {
if (candidateLocale == currentLocaleAndName.locale())
return rawLink;
Document targetDoc = localizedDocument.getDocuments().get(candidateLocale);
if (targetDoc != null) {
splitPath[splitPath.length - 1] = targetDoc.file().getFileName().toString();
return String.join("/", splitPath);
}
}
}
return rawLink;
})).append('\n');
}
private void updateDocument(Document document) throws IOException {
StringBuilder outputBuilder = new StringBuilder(8192);
for (Document.Item item : document.items()) {
if (item instanceof Document.Line line) {
processLine(outputBuilder, line.content(), document);
MacroProcessor.processLine(outputBuilder, line.content(), document);
} else if (item instanceof Document.MacroBlock macro) {
var processor = MacroProcessor.valueOf(macro.name());
processor.apply(document, macro, outputBuilder);

View File

@ -4,6 +4,8 @@
**English** | 中文 ([简体](PLATFORM_zh.md), [繁體](PLATFORM_zh_Hant.md))
<!-- #END LANGUAGE_SWITCHER -->
<!-- #BEGIN BLOCK -->
<!-- #PROPERTY NAME=PLATFORM_TABLE -->
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
@ -16,6 +18,7 @@
| LoongArch64 (Old World) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
<!-- #END BLOCK -->
Legend:

View File

@ -4,18 +4,23 @@
[English](PLATFORM.md) | **中文** (**简体**, [繁體](PLATFORM_zh_Hant.md))
<!-- #END LANGUAGE_SWITCHER -->
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM32 | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=PLATFORM_TABLE -->
<!-- #PROPERTY REPLACE="\\(Old World\\)" "(旧世界)" -->
<!-- #PROPERTY REPLACE="\\(use Rosetta 2\\)" "(使用 Rosetta 2)" -->
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM32 | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
<!-- #END COPY -->
图例:

View File

@ -4,18 +4,23 @@
[English](PLATFORM.md) | **中文** ([简体](PLATFORM_zh.md), **繁體**)
<!-- #END LANGUAGE_SWITCHER -->
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM32 | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=PLATFORM_TABLE -->
<!-- #PROPERTY REPLACE="\\(Old World\\)" "(舊世界)" -->
<!-- #PROPERTY REPLACE="\\(use Rosetta 2\\)" "(使用 Rosetta 2)" -->
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM32 | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
<!-- #END COPY -->
圖例:

View File

@ -1,10 +1,13 @@
# Hello Minecraft! Launcher
<!-- #BEGIN BLOCK -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END BLOCK -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
**English** | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)

View File

@ -1,10 +1,14 @@
# Hello Minecraft! Launcher
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | **español** | [русский](README_ru.md) | [українська](README_uk.md)

View File

@ -1,10 +1,13 @@
# Hello Minecraft! Launcher
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | **日本語** | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)

View File

@ -1,10 +1,13 @@
# Hello Minecraft! Launcher
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)

View File

@ -1,10 +1,13 @@
# Hello Minecraft! Launcher
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | **русский** | [українська](README_uk.md)

View File

@ -1,10 +1,13 @@
# Hello Minecraft! Launcher
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | **українська**

View File

@ -1,10 +1,13 @@
# Hello Minecraft! Launcher
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | **中文** (**简体**, [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)

View File

@ -1,10 +1,13 @@
# Hello Minecraft! Launcher
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | **中文** ([简体](README_zh.md), **繁體**, [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)