添加翻译功能和多语言支持
在 `I18nChatClient.cs` 中实现了与火山方舟API的交互,支持文本翻译的同步和异步方法,并增加了请求重试机制。更新了项目文件以引入必要的依赖包。对UI进行了修改,添加了翻译按钮并重构布局。扩展了视图模型以支持翻译逻辑,更新章节和任务的翻译属性。新增多个模型以支持多语言功能,并在服务类中实现了FTB任务的解析和多语言文件的导入。最后,调整了文件写入编码以确保兼容性。
This commit is contained in:
parent
6edc4897fa
commit
c59bb3e007
|
@ -0,0 +1,146 @@
|
|||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using System;
|
||||
using System.ClientModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MCI18n.AI
|
||||
{
|
||||
internal class I18nChatClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 火山方舟API Key
|
||||
/// </summary>
|
||||
private string API_KEY = "API_KEY";
|
||||
private string API_ENDPOINT = "https://ark.cn-beijing.volces.com/api/v3";
|
||||
private readonly ChatClient _client;
|
||||
|
||||
// 使用信号量限制并发请求数
|
||||
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(2, 3); // 最多2个并发请求,可根据API限制调整
|
||||
|
||||
// 重试次数和延迟
|
||||
private const int MaxRetries = 3;
|
||||
private static readonly TimeSpan InitialRetryDelay = TimeSpan.FromSeconds(1);
|
||||
|
||||
public I18nChatClient(string modelName)
|
||||
{
|
||||
_client = new ChatClient(modelName, new ApiKeyCredential(API_KEY), new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri(API_ENDPOINT),
|
||||
NetworkTimeout = TimeSpan.FromSeconds(20d)
|
||||
});
|
||||
}
|
||||
|
||||
private string SystemPrompt = @"
|
||||
你是一个minecraft相关的语言翻译机器人。
|
||||
|
||||
你需要翻译用户提供的文本到指定的目标语言。
|
||||
|
||||
源语言通常是英文。
|
||||
|
||||
你只需要翻译文本,禁止解释或提供额外信息。
|
||||
|
||||
如果存在多种解释或是含义,只需要给出最贴近Minecraft与社区Mod相关语言的一个。
|
||||
|
||||
被{}包裹的内容保留原文与位置。
|
||||
";
|
||||
private string Prompt = @"
|
||||
|
||||
目标语言:{0}
|
||||
|
||||
用户提供的文本:
|
||||
{1}
|
||||
";
|
||||
|
||||
public string Translate(string text, string targetLanguage)
|
||||
{
|
||||
try
|
||||
{
|
||||
_semaphore.Wait(); // 获取信号量
|
||||
try
|
||||
{
|
||||
var quest = string.Format(Prompt, targetLanguage, text);
|
||||
|
||||
var response = _client.CompleteChat(
|
||||
ChatMessage.CreateSystemMessage(SystemPrompt),
|
||||
ChatMessage.CreateUserMessage($"{quest}")
|
||||
);
|
||||
if (!string.IsNullOrWhiteSpace(response.Value.Content[0].Text))
|
||||
{
|
||||
return response.Value.Content[0].Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("No response from translation model.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release(); // 释放信号量
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> TranslateAsync(string text, string targetLanguage)
|
||||
{
|
||||
int retries = 0;
|
||||
TimeSpan delay = InitialRetryDelay;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(); // 异步获取信号量
|
||||
try
|
||||
{
|
||||
var quest = string.Format(Prompt, targetLanguage, text);
|
||||
|
||||
var response = await _client.CompleteChatAsync(
|
||||
ChatMessage.CreateSystemMessage(SystemPrompt),
|
||||
ChatMessage.CreateUserMessage($"{quest}")
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(response.Value.Content[0].Text))
|
||||
{
|
||||
return response.Value.Content[0].Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("No response from translation model.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release(); // 释放信号量
|
||||
}
|
||||
}
|
||||
catch (ClientResultException ex) when (ex.Status == 429 && retries < MaxRetries)
|
||||
{
|
||||
// 针对429错误特殊处理,采用退避策略
|
||||
retries++;
|
||||
await Task.Delay(delay);
|
||||
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // 指数退避
|
||||
continue; // 重试请求
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
break; // 如果成功或者非429错误,退出循环
|
||||
}
|
||||
|
||||
// 这里不会被执行到,只是为了满足编译器要求
|
||||
throw new Exception("Unexpected code path");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,9 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" Version="9.6.0" />
|
||||
<PackageReference Include="OpenAI" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCI18n", "MCI18n.csproj", "
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpNBT", "..\SharpNBT\SharpNBT\SharpNBT.csproj", "{173F7856-B894-5F24-D410-91B18378A44B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "..\ConsoleApp1\ConsoleApp1.csproj", "{2B780307-A7BB-4037-8C96-225544BB6C7B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -21,6 +23,10 @@ Global
|
|||
{173F7856-B894-5F24-D410-91B18378A44B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{173F7856-B894-5F24-D410-91B18378A44B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{173F7856-B894-5F24-D410-91B18378A44B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2B780307-A7BB-4037-8C96-225544BB6C7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2B780307-A7BB-4037-8C96-225544BB6C7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2B780307-A7BB-4037-8C96-225544BB6C7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2B780307-A7BB-4037-8C96-225544BB6C7B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
@ -49,7 +49,9 @@
|
|||
Command="{Binding SaveQuestCommand}"
|
||||
Content="打包"
|
||||
IsEnabled="False" />
|
||||
|
||||
<StackPanel Visibility="{}">
|
||||
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<TabControl Grid.Row="1">
|
||||
<TabItem Header="任务列表">
|
||||
|
@ -73,40 +75,55 @@
|
|||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<ScrollViewer Grid.Column="1">
|
||||
<ItemsControl ItemsSource="{Binding SelectedQuests}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<GroupBox>
|
||||
<GroupBox.Header>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button
|
||||
Width="100"
|
||||
Height="30"
|
||||
Command="{Binding TranslateTheChapterCommand}"
|
||||
Content="全部翻译(AI)"
|
||||
FontSize="14" />
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ItemsControl ItemsSource="{Binding SelectedQuests}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<GroupBox>
|
||||
<GroupBox.Header>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBox
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
FontSize="12"
|
||||
Foreground="White"
|
||||
Text="{Binding Title}" />
|
||||
<TextBox
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
FontSize="12"
|
||||
Foreground="LightGray"
|
||||
Text="{Binding SubTitle}" />
|
||||
</StackPanel>
|
||||
</GroupBox.Header>
|
||||
<StackPanel>
|
||||
<TextBlock Text="描述:" />
|
||||
<TextBox
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
FontSize="12"
|
||||
Foreground="White"
|
||||
Text="{Binding Title}" />
|
||||
<TextBox
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
FontSize="12"
|
||||
Foreground="LightGray"
|
||||
Text="{Binding SubTitle}" />
|
||||
AcceptsReturn="True"
|
||||
Text="{Binding Description}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</GroupBox.Header>
|
||||
<StackPanel>
|
||||
<TextBlock Text="描述:" />
|
||||
<TextBox
|
||||
AcceptsReturn="True"
|
||||
Text="{Binding Description}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="战利品列表" />
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MCI18n.AI;
|
||||
using MCI18n.Models;
|
||||
using MCI18n.Services;
|
||||
using MCI18n.Utilities;
|
||||
using Microsoft.Win32;
|
||||
using SharpNBT;
|
||||
|
@ -25,14 +27,15 @@ namespace MCI18n
|
|||
private List<QuestLangModel> _langs;
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<ChapterModel> _chapters;
|
||||
|
||||
|
||||
private FTBQuestsService _service;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<QuestModel> _selectedQuests;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _loading = false;
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private void LoadQuests()
|
||||
|
@ -48,131 +51,16 @@ namespace MCI18n
|
|||
try
|
||||
{
|
||||
var folderPath = openFolderDialog.FolderName;
|
||||
GlobalContext.MultipleLangSupport = Path.Exists(Path.Combine(folderPath, "quests", "lang"));
|
||||
if (GlobalContext.MultipleLangSupport)
|
||||
_service = new FTBQuestsService(folderPath);
|
||||
var ftbQuests = _service.Parse();
|
||||
Chapters = [];
|
||||
foreach (var group in ftbQuests.Groups)
|
||||
{
|
||||
var enLangFile = Path.Combine(folderPath, "quests", "lang", "en_us.snbt");
|
||||
var langData = StringNbt.Parse(File.ReadAllText(enLangFile));
|
||||
_langs = [];
|
||||
foreach (var item in langData)
|
||||
foreach (var chapter in group.Chapters)
|
||||
{
|
||||
var langKeys = item.Name.Split('.');
|
||||
var langItem = new QuestLangModel
|
||||
{
|
||||
Type = langKeys[0],
|
||||
Id = langKeys[1],
|
||||
DocType = langKeys[2],
|
||||
};
|
||||
if (item is StringTag langTag)
|
||||
{
|
||||
langItem.Value = langTag.Value;
|
||||
}
|
||||
else if(item is ListTag valueList)
|
||||
{
|
||||
var descList = new StringBuilder();
|
||||
foreach (var value in valueList)
|
||||
{
|
||||
descList.AppendLine((value as StringTag).Value);
|
||||
}
|
||||
langItem.Value = descList.ToString();
|
||||
}
|
||||
_langs.Add(langItem);
|
||||
Chapters.Add(chapter);
|
||||
}
|
||||
}
|
||||
|
||||
if(_langs != null)
|
||||
{
|
||||
var windowTitle = "FTB Quests Editor - " + _langs.FirstOrDefault(it => it.Type == "file" && it.Id == "0000000000000001" && it.DocType == "title").Value;
|
||||
Application.Current.MainWindow.Title = windowTitle;
|
||||
}
|
||||
if (Path.Exists(Path.Combine(folderPath, "quests", "chapters")))
|
||||
{
|
||||
var fullQuestsPath = Path.Combine(folderPath, "quests", "chapters");
|
||||
var questFiles = Directory.GetFiles(fullQuestsPath, "*.snbt", SearchOption.AllDirectories);
|
||||
_oriQuestsData = new List<CompoundTag>();
|
||||
_updatedChapters = new List<string>();
|
||||
Chapters = new ObservableCollection<ChapterModel>();
|
||||
foreach (var file in questFiles)
|
||||
{
|
||||
var questData = StringNbt.Parse(File.ReadAllText(file));
|
||||
_oriQuestsData.Add(questData);
|
||||
var chapterId = (questData["id"] as StringTag)?.Value;
|
||||
var chapterName = (questData["filename"] as StringTag)?.Value ;
|
||||
var chapterTitle = questData.ContainsKey("title") ? (questData["title"] as StringTag)?.Value : "";
|
||||
var chapter = new ChapterModel
|
||||
{
|
||||
Id = chapterId,
|
||||
Name = chapterName,
|
||||
Title = chapterTitle
|
||||
};
|
||||
if (GlobalContext.MultipleLangSupport)
|
||||
{
|
||||
chapter.Title = GetMutliLang("chapter",chapter.Id, "title");
|
||||
}
|
||||
Chapters.Add(chapter);
|
||||
var quests = questData["quests"];
|
||||
if(quests is ListTag questList)
|
||||
{
|
||||
foreach (var item in questList)
|
||||
{
|
||||
if (item is CompoundTag questTag)
|
||||
{
|
||||
var questId = (questTag["id"] as StringTag)?.Value;
|
||||
var questTitle = "";
|
||||
if (questTag.ContainsKey("title"))
|
||||
{
|
||||
questTitle = (questTag["title"] as StringTag)?.Value ?? "";
|
||||
}
|
||||
else if (GlobalContext.MultipleLangSupport)
|
||||
{
|
||||
questTitle = GetMutliLang("quest", questId, "title");
|
||||
}
|
||||
var questSubTitle = "";
|
||||
if (questTag.ContainsKey("subtitle"))
|
||||
{
|
||||
questSubTitle = (questTag["subtitle"] as StringTag)?.Value ?? "";
|
||||
}
|
||||
else if (GlobalContext.MultipleLangSupport)
|
||||
{
|
||||
questSubTitle = GetMutliLang("quest", questId, "quest_subtitle");
|
||||
}
|
||||
var questDescriptions = new StringBuilder();
|
||||
if (questTag.ContainsKey("description"))
|
||||
{
|
||||
if (questTag["description"] is ListTag descriptions)
|
||||
{
|
||||
foreach (var desc in descriptions)
|
||||
{
|
||||
questDescriptions.AppendLine((desc as StringTag)?.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (GlobalContext.MultipleLangSupport)
|
||||
{
|
||||
var desc = GetMutliLang("quest", questId, "quest_desc");
|
||||
questDescriptions.AppendLine(desc);
|
||||
}
|
||||
|
||||
var questModel = new QuestModel
|
||||
{
|
||||
Id = questId,
|
||||
Title = questTitle,
|
||||
SubTitle = questSubTitle,
|
||||
Description = questDescriptions.ToString()
|
||||
};
|
||||
chapter.Quests.Add(questModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SelectedQuests = Chapters.First()?.Quests ?? [];
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("文件夹选择错误!");
|
||||
}
|
||||
//var questData = StringNbt.Parse(File.ReadAllText(CurrentFilePath));
|
||||
//SnbtWriter.SaveToFile(questData, CurrentFilePath + ".r", prettyPrint: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -184,7 +72,7 @@ namespace MCI18n
|
|||
[RelayCommand]
|
||||
private void SaveQuest()
|
||||
{
|
||||
|
||||
//TODO Sava ALL (非多语言)
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
@ -206,6 +94,7 @@ namespace MCI18n
|
|||
{
|
||||
var filePath = saveFileDialog.FileName;
|
||||
var langData = new CompoundTag("");
|
||||
langData.Add(new StringTag("file.0000000000000001.title", Application.Current.MainWindow.Title));
|
||||
foreach (var chapter in Chapters)
|
||||
{
|
||||
langData.Add(new StringTag($"chapter.{chapter.Id}.title", chapter.Title));
|
||||
|
@ -214,12 +103,16 @@ namespace MCI18n
|
|||
foreach (var quest in chapter.Quests)
|
||||
{
|
||||
langData.Add(new StringTag($"quest.{quest.Id}.title", quest.Title));
|
||||
langData.Add(new StringTag($"quest.{quest.Id}.quest_subtitle", quest.SubTitle));
|
||||
if (!string.IsNullOrWhiteSpace(quest.SubTitle))
|
||||
{
|
||||
langData.Add(new StringTag($"quest.{quest.Id}.quest_subtitle", quest.SubTitle));
|
||||
}
|
||||
var descLines = quest.Description.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
||||
var descList = new ListTag($"quest.{quest.Id}.quest_desc",TagType.String);
|
||||
foreach (var line in descLines)
|
||||
{
|
||||
descList.Add(new StringTag(null, line));
|
||||
if(!string.IsNullOrWhiteSpace(line))
|
||||
descList.Add(new StringTag(null, line));
|
||||
}
|
||||
langData.Add(descList);
|
||||
}
|
||||
|
@ -242,6 +135,51 @@ namespace MCI18n
|
|||
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task TranslateTheChapter()
|
||||
{
|
||||
Loading = true;
|
||||
try
|
||||
{
|
||||
var aiClient = new I18nChatClient("doubao-1-5-pro-32k-250115");
|
||||
|
||||
var translationTasks = new List<Task>();
|
||||
|
||||
foreach (var item in SelectedQuests)
|
||||
{
|
||||
translationTasks.Add(TranslateQuestAsync(item, aiClient));
|
||||
}
|
||||
|
||||
await Task.WhenAll(translationTasks);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TranslateQuestAsync(QuestModel quest, I18nChatClient aiClient)
|
||||
{
|
||||
var titleTask = !string.IsNullOrWhiteSpace(quest.Title)
|
||||
? aiClient.TranslateAsync(quest.Title, "zh_cn")
|
||||
: Task.FromResult<string>(quest.Title);
|
||||
|
||||
var subtitleTask = !string.IsNullOrWhiteSpace(quest.SubTitle)
|
||||
? aiClient.TranslateAsync(quest.SubTitle, "zh_cn")
|
||||
: Task.FromResult<string>(quest.SubTitle);
|
||||
|
||||
var descriptionTask = !string.IsNullOrWhiteSpace(quest.Description)
|
||||
? aiClient.TranslateAsync(quest.Description, "zh_cn")
|
||||
: Task.FromResult<string>(quest.Description);
|
||||
|
||||
// 等待所有翻译任务完成
|
||||
await Task.WhenAll(titleTask, subtitleTask, descriptionTask);
|
||||
|
||||
// 更新属性
|
||||
quest.Title = await titleTask;
|
||||
quest.SubTitle = await subtitleTask;
|
||||
quest.Description = await descriptionTask;
|
||||
}
|
||||
internal void ChapterSelectionChanged(ChapterModel selectedChapter)
|
||||
{
|
||||
if (selectedChapter != null && selectedChapter.Quests != null)
|
||||
|
@ -253,10 +191,6 @@ namespace MCI18n
|
|||
SelectedQuests = [];
|
||||
}
|
||||
|
||||
}
|
||||
private string GetMutliLang(string type,string id,string docType)
|
||||
{
|
||||
return _langs?.FirstOrDefault(it => it.Type == type && it.Id == id && it.DocType == docType)?.Value ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Documents;
|
||||
|
||||
namespace MCI18n.Models
|
||||
{
|
||||
public partial class ChapterGroupModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _id;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _name;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<ChapterModel> _chapters = [];
|
||||
}
|
||||
}
|
|
@ -12,11 +12,14 @@ namespace MCI18n.Models
|
|||
{
|
||||
[ObservableProperty]
|
||||
private string _id;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _groupId;
|
||||
[ObservableProperty]
|
||||
private string _name;
|
||||
[ObservableProperty]
|
||||
private string _title;
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<QuestModel> _quests = new ObservableCollection<QuestModel>();
|
||||
private ObservableCollection<QuestModel> _quests = [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MCI18n.Models
|
||||
{
|
||||
public partial class FTBQuestsModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _id;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _title;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<ChapterGroupModel> _groups = [];
|
||||
}
|
||||
}
|
|
@ -15,7 +15,15 @@ namespace MCI18n.Models
|
|||
private string _id;
|
||||
[ObservableProperty]
|
||||
private string _docType;
|
||||
/// <summary>
|
||||
/// 多语言内容
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _value;
|
||||
/// <summary>
|
||||
/// 当导入多语言时,保留原文
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _originalValue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,395 @@
|
|||
using MCI18n.Models;
|
||||
using MCI18n.Utilities;
|
||||
using SharpNBT;
|
||||
using SharpNBT.SNBT;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace MCI18n.Services
|
||||
{
|
||||
public class FTBQuestsService
|
||||
{
|
||||
private string _ftbQuestsPath;
|
||||
private bool _multiLanguageSupportEnabled = false;
|
||||
|
||||
private FTBQuestsModel FTBQuests;
|
||||
|
||||
private List<QuestLangModel> _langs = [];
|
||||
|
||||
|
||||
private string OriginalLang = "en_us";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
/// <param name="ftbQuestsPath">eg: ftbquests/quests</param>
|
||||
public FTBQuestsService(string ftbQuestsPath)
|
||||
{
|
||||
|
||||
_ftbQuestsPath = ftbQuestsPath;
|
||||
CheckMultiLanguageSupport();
|
||||
FTBQuests = new FTBQuestsModel();
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// 设置源语言代码
|
||||
/// </summary>
|
||||
/// <param name="langCode"></param>
|
||||
/// <returns></returns>
|
||||
public FTBQuestsService SetOriginalLang(string langCode)
|
||||
{
|
||||
OriginalLang = langCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否支持多语言
|
||||
/// </summary>
|
||||
public void CheckMultiLanguageSupport()
|
||||
{
|
||||
_multiLanguageSupportEnabled = Path.Exists(Path.Combine(_ftbQuestsPath, Consts.FTBQuestsLangPath));
|
||||
GlobalContext.MultipleLangSupport = _multiLanguageSupportEnabled;
|
||||
}
|
||||
|
||||
public FTBQuestsModel Parse()
|
||||
{
|
||||
ParseData();
|
||||
ParseGroup();
|
||||
ParseDefaultLang();
|
||||
ParseFTBQuests();
|
||||
//if (_multiLanguageSupportEnabled)
|
||||
//{
|
||||
// ParseExistsLang(OriginalLang);
|
||||
//}
|
||||
return FTBQuests;
|
||||
}
|
||||
|
||||
public void ParseData()
|
||||
{
|
||||
var dataFile = Path.Combine(_ftbQuestsPath, "data.snbt");
|
||||
if (File.Exists(dataFile))
|
||||
{
|
||||
var dataContent = StringNbt.Parse(File.ReadAllText(dataFile));
|
||||
if (dataContent.ContainsKey("ftbquests"))
|
||||
{
|
||||
var ftbQuestsData = dataContent["ftbquests"] as CompoundTag;
|
||||
if (ftbQuestsData != null)
|
||||
{
|
||||
FTBQuests.Id = ftbQuestsData["id"] as StringTag ?? "";
|
||||
if (!_multiLanguageSupportEnabled)
|
||||
{
|
||||
FTBQuests.Title = ftbQuestsData.ContainsKey("title") ? (ftbQuestsData["title"] as StringTag)?.Value ?? "" : "";
|
||||
}
|
||||
else
|
||||
{
|
||||
FTBQuests.Title = GetMutliLang("0000000000000001", "file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ParseGroup()
|
||||
{
|
||||
var groupFile = Path.Combine(_ftbQuestsPath,"quests", "chapter_groups.snbt");
|
||||
if (File.Exists(groupFile))
|
||||
{
|
||||
var groupData = StringNbt.Parse(File.ReadAllText(groupFile));
|
||||
var groups = new ObservableCollection<ChapterGroupModel>();
|
||||
foreach (var item in groupData)
|
||||
{
|
||||
if (item is ListTag groupTags)
|
||||
{
|
||||
foreach(var group in groupTags)
|
||||
{
|
||||
if(group is CompoundTag groupTag)
|
||||
{
|
||||
var groupModel = new ChapterGroupModel
|
||||
{
|
||||
Id = groupTag["id"] as StringTag,
|
||||
Chapters = new ObservableCollection<ChapterModel>()
|
||||
};
|
||||
groups.Add(groupModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FTBQuests.Groups = groups;
|
||||
}
|
||||
}
|
||||
public void ParseDefaultLang()
|
||||
{
|
||||
if (_multiLanguageSupportEnabled && ExistsMultiLangFiles())
|
||||
{
|
||||
var enLangFile = Path.Combine(_ftbQuestsPath, Consts.FTBQuestsLangPath, "en_us.snbt");
|
||||
var langData = StringNbt.Parse(File.ReadAllText(enLangFile));
|
||||
|
||||
foreach (var item in langData)
|
||||
{
|
||||
if (item == null) continue;
|
||||
var langItem = ParseLangData(item);
|
||||
_langs.Add(langItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void ParseFTBQuests()
|
||||
{
|
||||
if (Path.Exists(Path.Combine(_ftbQuestsPath, "quests", "chapters")))
|
||||
{
|
||||
var fullQuestsPath = Path.Combine(_ftbQuestsPath, "quests", "chapters");
|
||||
var questFiles = Directory.GetFiles(fullQuestsPath, "*.snbt", SearchOption.AllDirectories);
|
||||
var chapters = new ObservableCollection<ChapterModel>();
|
||||
foreach (var file in questFiles)
|
||||
{
|
||||
var questData = StringNbt.Parse(File.ReadAllText(file));
|
||||
//_oriQuestsData.Add(questData);
|
||||
var chapter = ParseChapter(questData);
|
||||
|
||||
chapters.Add(chapter);
|
||||
|
||||
var quests = questData["quests"];
|
||||
if (quests is ListTag questList)
|
||||
{
|
||||
foreach (var item in questList)
|
||||
{
|
||||
if (item is CompoundTag questTag)
|
||||
{
|
||||
var questModel = ParseQuest(questTag);
|
||||
chapter.Quests.Add(questModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
foreach (var group in FTBQuests.Groups)
|
||||
{
|
||||
if(chapter.GroupId == group.Id)
|
||||
{
|
||||
group.Chapters.Add(chapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("文件夹选择错误!");
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 解析多语言数据
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <returns></returns>
|
||||
public QuestLangModel ParseLangData(Tag item)
|
||||
{
|
||||
var langKeys = item!.Name.Split('.');
|
||||
var langItem = new QuestLangModel
|
||||
{
|
||||
Type = langKeys[0],
|
||||
Id = langKeys[1],
|
||||
DocType = langKeys[2],
|
||||
};
|
||||
if (item is StringTag langTag)
|
||||
{
|
||||
langItem.Value = langTag.Value;
|
||||
}
|
||||
else if (item is ListTag valueList)
|
||||
{
|
||||
var descList = new StringBuilder();
|
||||
foreach (var value in valueList)
|
||||
{
|
||||
descList.AppendLine((value as StringTag).Value);
|
||||
}
|
||||
langItem.Value = descList.ToString();
|
||||
}
|
||||
|
||||
return langItem;
|
||||
}
|
||||
/// <summary>
|
||||
/// 解析章节数据
|
||||
/// </summary>
|
||||
/// <param name="questData"></param>
|
||||
/// <returns></returns>
|
||||
public ChapterModel ParseChapter(CompoundTag questData)
|
||||
{
|
||||
var chapterId = (questData["id"] as StringTag)?.Value ?? "";
|
||||
var chapterName = (questData["filename"] as StringTag)?.Value ?? "";
|
||||
var groupId = (questData["group"] as StringTag)?.Value ?? "";
|
||||
var chapterTitle = questData.ContainsKey("title")
|
||||
? (questData["title"] as StringTag)?.Value ?? ""
|
||||
: "";
|
||||
var chapter = new ChapterModel
|
||||
{
|
||||
Id = chapterId,
|
||||
GroupId = groupId,
|
||||
Name = chapterName,
|
||||
Title = chapterTitle
|
||||
};
|
||||
if (_multiLanguageSupportEnabled)
|
||||
{
|
||||
chapter.Title = GetMutliLang(chapter.Id,"chapter", "title");
|
||||
}
|
||||
return chapter;
|
||||
}
|
||||
/// <summary>
|
||||
/// 解析任务数据
|
||||
/// </summary>
|
||||
/// <param name="questTag"></param>
|
||||
/// <returns></returns>
|
||||
public QuestModel ParseQuest(CompoundTag questTag)
|
||||
{
|
||||
var questId = (questTag["id"] as StringTag)?.Value ?? "";
|
||||
|
||||
var questTitle = "";
|
||||
if (questTag.ContainsKey("title"))
|
||||
{
|
||||
questTitle = (questTag["title"] as StringTag)?.Value ?? "";
|
||||
}
|
||||
else if (_multiLanguageSupportEnabled)
|
||||
{
|
||||
questTitle = GetMutliLang(questId, "quest", "title");
|
||||
}
|
||||
|
||||
var questSubTitle = "";
|
||||
if (questTag.ContainsKey("subtitle"))
|
||||
{
|
||||
questSubTitle = (questTag["subtitle"] as StringTag)?.Value ?? "";
|
||||
}
|
||||
else if (_multiLanguageSupportEnabled)
|
||||
{
|
||||
questSubTitle = GetMutliLang(questId, "quest", "quest_subtitle");
|
||||
}
|
||||
|
||||
var questDescriptions = new StringBuilder();
|
||||
if (questTag.ContainsKey("description"))
|
||||
{
|
||||
if (questTag["description"] is ListTag descriptions)
|
||||
{
|
||||
foreach (var desc in descriptions)
|
||||
{
|
||||
questDescriptions.AppendLine((desc as StringTag)?.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_multiLanguageSupportEnabled)
|
||||
{
|
||||
var desc = GetMutliLang(questId, "quest", "quest_desc");
|
||||
questDescriptions.AppendLine(desc);
|
||||
}
|
||||
|
||||
var questModel = new QuestModel
|
||||
{
|
||||
Id = questId,
|
||||
Title = questTitle,
|
||||
SubTitle = questSubTitle,
|
||||
Description = questDescriptions.ToString(),
|
||||
Tasks = []
|
||||
};
|
||||
if (questTag.ContainsKey("tasks"))
|
||||
{
|
||||
var tasks = questTag["tasks"] as ListTag;
|
||||
if (tasks != null)
|
||||
{
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
if (task is CompoundTag taskTag)
|
||||
{
|
||||
var taskModel = new TaskModel
|
||||
{
|
||||
Id = taskTag["id"] as StringTag ?? "",
|
||||
};
|
||||
if (taskTag.ContainsKey("title"))
|
||||
{
|
||||
taskModel.Title = (taskTag["title"] as StringTag)?.Value ?? "";
|
||||
}
|
||||
else if (_multiLanguageSupportEnabled)
|
||||
{
|
||||
taskModel.Title = GetMutliLang(taskModel.Id, "task", "title");
|
||||
}
|
||||
questModel.Tasks.Add(taskModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return questModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入已有的多语言文件
|
||||
/// </summary>
|
||||
/// <param name="targetLangCode"></param>
|
||||
public void ParseExistsLang(string targetLangCode)
|
||||
{
|
||||
if (GlobalContext.MultipleLangSupport)
|
||||
{
|
||||
var langFile = Path.Combine(_ftbQuestsPath, Consts.FTBQuestsLangPath, $"{targetLangCode}.snbt");
|
||||
if (File.Exists(langFile))
|
||||
{
|
||||
|
||||
var langData = StringNbt.Parse(File.ReadAllText(langFile));
|
||||
|
||||
foreach (var item in langData)
|
||||
{
|
||||
if (item == null) continue;
|
||||
var langItem = ParseLangData(item);
|
||||
UpdateLang(langItem.Id, langItem.Value, langItem.Type, langItem.DocType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取多语言文本
|
||||
/// </summary>
|
||||
/// <param name="id">0000000000000001</param>
|
||||
/// <param name="type">file</param>
|
||||
/// <param name="docType">title</param>
|
||||
/// <returns></returns>
|
||||
public string GetMutliLang(string id, string type = "quests", string docType = "title")
|
||||
{
|
||||
return _langs?.FirstOrDefault(it => it.Type == type && it.Id == id && it.DocType == docType)?.Value ?? "";
|
||||
}
|
||||
/// <summary>
|
||||
/// 更新多语言文本
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="docType"></param>
|
||||
public void UpdateLang(string id, string value, string type = "quests", string docType = "title")
|
||||
{
|
||||
var langItem = _langs.FirstOrDefault(it => it.Type == type && it.Id == id && it.DocType == docType);
|
||||
if (langItem != null)
|
||||
{
|
||||
langItem.OriginalValue = langItem.Value;
|
||||
langItem.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_langs.Add(new QuestLangModel
|
||||
{
|
||||
Type = type,
|
||||
Id = id,
|
||||
DocType = docType,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 是否存在多语言文件夹
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool ExistsMultiLangFiles()
|
||||
{
|
||||
return Path.Exists(Path.Combine(_ftbQuestsPath, Consts.FTBQuestsLangPath));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MCI18n.Utilities
|
||||
{
|
||||
public class Consts
|
||||
{
|
||||
public const string FTBQuestsLangPath = "quests/lang";
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ namespace MCI18n.Utilities
|
|||
try
|
||||
{
|
||||
string snbt = ConvertToSnbt(tag, prettyPrint);
|
||||
File.WriteAllText(filePath, snbt, Encoding.UTF8);
|
||||
File.WriteAllText(filePath, snbt, new UTF8Encoding(false));
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or DirectoryNotFoundException)
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue