Skip to content

Commit 9ecf335

Browse files
committed
Обновление сценариев, рабочих процессов и перевода
1 parent dceabd6 commit 9ecf335

11 files changed

Lines changed: 857 additions & 28 deletions

File tree

.github/go/common/utils.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package common
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// GetValueAsString получает строковое значение из строки таблицы по индексу колонки
9+
func GetValueAsString(row []interface{}, columnIndex int) string {
10+
if columnIndex >= len(row) {
11+
return ""
12+
}
13+
return strings.TrimSpace(fmt.Sprintf("%v", row[columnIndex]))
14+
}

.github/go/generate_mod_list.go

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"os"
10+
"sort"
11+
"strings"
12+
13+
"golang.org/x/oauth2/google"
14+
"google.golang.org/api/option"
15+
"google.golang.org/api/sheets/v4"
16+
17+
"github.com/RushanM/Minecraft-Mods-Russian-Translation/tools/common"
18+
)
19+
20+
// ProofreadDates содержит даты последней проверки для модов
21+
type ProofreadDates map[string]string
22+
23+
// ModInfo содержит информацию о моде
24+
type ModInfo struct {
25+
Name string
26+
GameVer string
27+
Proofread string
28+
ModrinthID string
29+
CurseforgeID string
30+
FallbackURL string
31+
Status string
32+
URL string
33+
Entry string
34+
}
35+
36+
func GenerateModList() {
37+
// Загрузка предыдущих данных
38+
previousProofreadDates := make(ProofreadDates)
39+
prevDatesFile := "previous_proofread_dates.json"
40+
41+
if _, err := os.Stat(prevDatesFile); err == nil {
42+
data, err := ioutil.ReadFile(prevDatesFile)
43+
if err != nil {
44+
fmt.Printf("Ошибка при чтении %s: %v\n", prevDatesFile, err)
45+
} else {
46+
err = json.Unmarshal(data, &previousProofreadDates)
47+
if err != nil {
48+
fmt.Printf("Ошибка при разборе JSON в %s: %v\n", prevDatesFile, err)
49+
}
50+
}
51+
} else {
52+
fmt.Println("Файл previous_proofread_dates.json не найден. Предполагается первый запуск.")
53+
}
54+
55+
// Подключение к Google Sheets API
56+
ctx := context.Background()
57+
serviceAccountKey := os.Getenv("GOOGLE_SERVICE_ACCOUNT_KEY")
58+
if serviceAccountKey == "" {
59+
fmt.Println("Не установлена переменная окружения GOOGLE_SERVICE_ACCOUNT_KEY")
60+
os.Exit(1)
61+
}
62+
63+
config, err := google.JWTConfigFromJSON([]byte(serviceAccountKey), sheets.SpreadsheetsReadonlyScope)
64+
if err != nil {
65+
fmt.Printf("Не удалось создать конфигурацию JWT: %v\n", err)
66+
os.Exit(1)
67+
}
68+
69+
client := config.Client(ctx)
70+
srv, err := sheets.NewService(ctx, option.WithHTTPClient(client))
71+
if err != nil {
72+
fmt.Printf("Не удалось создать сервис Sheets: %v\n", err)
73+
os.Exit(1)
74+
}
75+
76+
// Получение данных из таблицы
77+
sheetID := "1kGGT2GGdG_Ed13gQfn01tDq2MZlVOC9AoiD1s3SDlZE"
78+
readRange := "Sheet1!A:Z"
79+
resp, err := srv.Spreadsheets.Values.Get(sheetID, readRange).Do()
80+
if err != nil {
81+
fmt.Printf("Не удалось получить данные из таблицы: %v\n", err)
82+
os.Exit(1)
83+
}
84+
85+
if len(resp.Values) == 0 {
86+
fmt.Println("Данные не найдены.")
87+
os.Exit(1)
88+
}
89+
90+
// Получение заголовков и индексов колонок
91+
headers := make(map[string]int)
92+
for i, header := range resp.Values[0] {
93+
headers[header.(string)] = i
94+
}
95+
96+
// Обработка данных
97+
modsByVersion := make(map[string][]ModInfo)
98+
currentProofreadDates := make(ProofreadDates)
99+
100+
for i := 1; i < len(resp.Values); i++ {
101+
row := resp.Values[i]
102+
if len(row) <= headers["proofread"] {
103+
continue
104+
}
105+
106+
proofread := common.GetValueAsString(row, headers["proofread"])
107+
if proofread == "" || strings.ToUpper(proofread) == "FALSE" {
108+
continue
109+
}
110+
111+
modName := common.GetValueAsString(row, headers["name"])
112+
gameVer := common.GetValueAsString(row, headers["gameVer"])
113+
modrinthID := common.GetValueAsString(row, headers["modrinthId"])
114+
curseforgeID := common.GetValueAsString(row, headers["curseforgeId"])
115+
fallbackURL := common.GetValueAsString(row, headers["fallbackUrl"])
116+
117+
// Сохранение текущей даты
118+
currentProofreadDates[modName] = proofread
119+
120+
// Определение статуса перевода
121+
status := "unchanged"
122+
prevProofreadDate, exists := previousProofreadDates[modName]
123+
if !exists {
124+
status = "new" // Новый мод
125+
} else if prevProofreadDate != proofread {
126+
status = "updated" // Обновлённый мод
127+
}
128+
129+
// Получение ссылки на мод
130+
modURL := getModURL(modrinthID, curseforgeID, fallbackURL)
131+
132+
// Формирование строки мода
133+
dateStr := fmt.Sprintf("<code>%s</code>", proofread)
134+
var modLink string
135+
if modURL != "" {
136+
modLink = fmt.Sprintf("<a href=\"%s\">%s</a>", modURL, modName)
137+
} else {
138+
modLink = modName
139+
}
140+
141+
// Добавление эмодзи и форматирования
142+
var modEntry string
143+
if status == "new" {
144+
emoji := "➕"
145+
modEntry = fmt.Sprintf("<li><b>%s %s %s</b></li>", emoji, modLink, dateStr)
146+
} else if status == "updated" {
147+
emoji := "✏️"
148+
modEntry = fmt.Sprintf("<li><b>%s %s %s</b></li>", emoji, modLink, dateStr)
149+
} else {
150+
modEntry = fmt.Sprintf("<li>%s %s</li>", modLink, dateStr)
151+
}
152+
153+
// Добавляем мод в соответствующую версию игры
154+
mod := ModInfo{
155+
Name: modName,
156+
GameVer: gameVer,
157+
Proofread: proofread,
158+
ModrinthID: modrinthID,
159+
CurseforgeID: curseforgeID,
160+
FallbackURL: fallbackURL,
161+
Status: status,
162+
URL: modURL,
163+
Entry: modEntry,
164+
}
165+
166+
modsByVersion[gameVer] = append(modsByVersion[gameVer], mod)
167+
}
168+
169+
// Генерация тела выпуска
170+
releaseBody := generateReleaseBody(modsByVersion)
171+
172+
// Сохранение текущих данных
173+
currentData, err := json.MarshalIndent(currentProofreadDates, "", " ")
174+
if err != nil {
175+
fmt.Printf("Не удалось сериализовать данные: %v\n", err)
176+
os.Exit(1)
177+
}
178+
179+
err = ioutil.WriteFile("current_proofread_dates.json", currentData, 0644)
180+
if err != nil {
181+
fmt.Printf("Не удалось записать данные в файл: %v\n", err)
182+
os.Exit(1)
183+
}
184+
185+
// Установка выходного значения для release_body
186+
githubOutput := os.Getenv("GITHUB_OUTPUT")
187+
if githubOutput != "" {
188+
file, err := os.OpenFile(githubOutput, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
189+
if err != nil {
190+
fmt.Printf("Не удалось открыть GITHUB_OUTPUT: %v\n", err)
191+
os.Exit(1)
192+
}
193+
defer file.Close()
194+
195+
_, err = file.WriteString("release_body<<EOF\n")
196+
if err != nil {
197+
fmt.Printf("Не удалось записать в GITHUB_OUTPUT: %v\n", err)
198+
os.Exit(1)
199+
}
200+
201+
_, err = file.WriteString(releaseBody)
202+
if err != nil {
203+
fmt.Printf("Не удалось записать release_body в GITHUB_OUTPUT: %v\n", err)
204+
os.Exit(1)
205+
}
206+
207+
_, err = file.WriteString("\nEOF\n")
208+
if err != nil {
209+
fmt.Printf("Не удалось закрыть EOF в GITHUB_OUTPUT: %v\n", err)
210+
os.Exit(1)
211+
}
212+
} else {
213+
fmt.Println(releaseBody)
214+
}
215+
}
216+
217+
// getModURL получает URL мода из API Modrinth или CurseForge
218+
func getModURL(modrinthID, curseforgeID, fallbackURL string) string {
219+
if modrinthID != "" && strings.ToUpper(modrinthID) != "FALSE" {
220+
// Modrinth
221+
resp, err := http.Get(fmt.Sprintf("https://api.modrinth.com/v2/project/%s", modrinthID))
222+
if err == nil && resp.StatusCode == http.StatusOK {
223+
var modData map[string]interface{}
224+
if err := json.NewDecoder(resp.Body).Decode(&modData); err == nil {
225+
if url, ok := modData["url"].(string); ok && url != "" {
226+
return url
227+
}
228+
}
229+
resp.Body.Close()
230+
}
231+
return fmt.Sprintf("https://modrinth.com/mod/%s", modrinthID)
232+
} else if curseforgeID != "" && strings.ToUpper(curseforgeID) != "FALSE" {
233+
// CurseForge
234+
apiKey := os.Getenv("CF_API_KEY")
235+
if apiKey != "" {
236+
client := &http.Client{}
237+
req, err := http.NewRequest("GET", fmt.Sprintf("https://api.curseforge.com/v1/mods/%s", curseforgeID), nil)
238+
if err == nil {
239+
req.Header.Add("Accept", "application/json")
240+
req.Header.Add("x-api-key", apiKey)
241+
resp, err := client.Do(req)
242+
if err == nil && resp.StatusCode == http.StatusOK {
243+
var cfResp struct {
244+
Data struct {
245+
Links struct {
246+
WebsiteURL string `json:"websiteUrl"`
247+
} `json:"links"`
248+
} `json:"data"`
249+
}
250+
if err := json.NewDecoder(resp.Body).Decode(&cfResp); err == nil {
251+
if cfResp.Data.Links.WebsiteURL != "" {
252+
return cfResp.Data.Links.WebsiteURL
253+
}
254+
}
255+
resp.Body.Close()
256+
}
257+
}
258+
}
259+
return fmt.Sprintf("https://www.curseforge.com/minecraft/mc-mods/%s", curseforgeID)
260+
} else if fallbackURL != "" && strings.ToUpper(fallbackURL) != "FALSE" {
261+
return fallbackURL // Используем ссылку из fallbackUrl
262+
}
263+
return "" // Не удалось получить ссылку
264+
}
265+
266+
// generateReleaseBody создаёт тело для выпуска в формате HTML/Markdown
267+
func generateReleaseBody(modsByVersion map[string][]ModInfo) string {
268+
// Начало тела выпуска
269+
releaseBody := `Это бета-выпуск всех переводов проекта. В отличие от альфа-выпуска, качество переводов здесь значительно выше, поскольку включены только те переводы, чьё качество достигло достаточно высокого уровня. Однако из-за этого охваченный спектр модов, сборок модов и наборов шейдеров значительно уже.
270+
271+
<details>
272+
<summary>
273+
<h3>🔠 Переведённые моды этого выпуска</h3>
274+
</summary>
275+
<br>
276+
<b>Условные обозначения</b>
277+
<br><br>
278+
<ul>
279+
<li>➕ — новый перевод</li>
280+
<li>✏️ — изменения в переводе</li>
281+
<li><code>ДД.ММ.ГГГГ</code> — дата последнего изменения</li>
282+
</ul>
283+
<br>
284+
`
285+
286+
// Сортировка версий игры
287+
gameVersions := make([]string, 0, len(modsByVersion))
288+
for gameVer := range modsByVersion {
289+
gameVersions = append(gameVersions, gameVer)
290+
}
291+
sort.Strings(gameVersions)
292+
293+
// Для каждой версии игры создаём спойлер
294+
for _, gameVer := range gameVersions {
295+
mods := modsByVersion[gameVer]
296+
297+
// Сортировка модов внутри версии по дате (новые выше)
298+
sort.Slice(mods, func(i, j int) bool {
299+
return mods[i].Proofread > mods[j].Proofread
300+
})
301+
302+
// Получаем последнюю дату для версии
303+
var latestDate string
304+
if len(mods) > 0 {
305+
latestDate = mods[0].Proofread
306+
}
307+
308+
// Определяем, есть ли новые или обновлённые моды
309+
versionStatus := ""
310+
for _, mod := range mods {
311+
if mod.Status == "new" || mod.Status == "updated" {
312+
versionStatus = "✏️"
313+
break
314+
}
315+
}
316+
317+
// Формируем заголовок спойлера для версии
318+
versionHeader := fmt.Sprintf("<summary><b>%s", gameVer)
319+
if versionStatus != "" {
320+
versionHeader += fmt.Sprintf(" %s", versionStatus)
321+
}
322+
versionHeader += fmt.Sprintf(" <code>%s</code></b></summary>", latestDate)
323+
releaseBody += fmt.Sprintf(" <details>\n %s\n <ul>\n", versionHeader)
324+
325+
// Добавляем моды
326+
for _, mod := range mods {
327+
releaseBody += fmt.Sprintf(" %s\n", mod.Entry)
328+
}
329+
330+
releaseBody += " </ul>\n </details>\n"
331+
}
332+
333+
releaseBody += "</details>\n\nЭтот выпуск является кандидатом на релиз. Если вы заметили какие-либо ошибки в этом выпуске, пожалуйста, сообщите об этом в разделе issues или отправьте сообщение [Дефлекте](https://github.com/RushanM)!"
334+
335+
return releaseBody
336+
}

.github/go/go.mod

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module github.com/RushanM/Minecraft-Mods-Russian-Translation/tools
2+
3+
go 1.24.2
4+
5+
require (
6+
golang.org/x/oauth2 v0.29.0
7+
google.golang.org/api v0.228.0
8+
)
9+
10+
require (
11+
cloud.google.com/go/auth v0.15.0 // indirect
12+
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
13+
cloud.google.com/go/compute/metadata v0.6.0 // indirect
14+
github.com/felixge/httpsnoop v1.0.4 // indirect
15+
github.com/go-logr/logr v1.4.2 // indirect
16+
github.com/go-logr/stdr v1.2.2 // indirect
17+
github.com/google/s2a-go v0.1.9 // indirect
18+
github.com/google/uuid v1.6.0 // indirect
19+
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
20+
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
21+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
22+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
23+
go.opentelemetry.io/otel v1.34.0 // indirect
24+
go.opentelemetry.io/otel/metric v1.34.0 // indirect
25+
go.opentelemetry.io/otel/trace v1.34.0 // indirect
26+
golang.org/x/crypto v0.37.0 // indirect
27+
golang.org/x/net v0.38.0 // indirect
28+
golang.org/x/sys v0.32.0 // indirect
29+
golang.org/x/text v0.24.0 // indirect
30+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
31+
google.golang.org/grpc v1.71.0 // indirect
32+
google.golang.org/protobuf v1.36.6 // indirect
33+
)

0 commit comments

Comments
 (0)