有时我们需要翻译 YAML
文件的内容,但目前缺乏一个专门针对 YAML
的便捷翻译工具。为此,我们可以自己开发一个定制化的工具。本文将介绍如何使用 Go
语言开发一个简单的 YAML
文件翻译工具。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
本文选择 deepl
提供的翻译 API
进行演示开发,因为即使你是个人开发者,也能免费享受到 deepl
每月提供的限量的文本翻译额度。
func readYAML(filename string) (*yaml.Node, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var yamlData yaml.Node
err = yaml.Unmarshal(data, &yamlData)
if err != nil {
return nil, err
}
return &yamlData, nil
}
该函数的作用是从指定的文件路径读取 YAML
文件内容并将其解析为 yaml.Node
对象:
os.ReadFile(filename)
读取指定文件路径下的文件,将其内容以字节数组的形式加载到内存中。yaml.Unmarshal
方法将读取的字节数据解析成 yaml.Node
对象。yaml.Node
是 Go YAML
包中的一种抽象数据结构,可以存储复杂的 YAML
层次结构,适合需要遍历和修改节点的情况。func writeYAML(filename string, node *yaml.Node) error {
data, err := yaml.Marshal(node)
if err != nil {
return err
}
return os.WriteFile(filename, data, 0644)
}
这个函数的作用是将修改过的 yaml.Node
对象重新序列化成 YAML
格式,并写回到文件中:
yaml.Marshal
将内存中的 yaml.Node
数据结构转换成字节数组。这是一个将 YAML
数据对象转换回可读文件格式的步骤。os.WriteFile
将序列化后的字节数组写入指定的文件路径,参数 0644
设置了文件的权限,表示文件所有者有读写权限,而其他用户只有读取权限。// 递归遍历并翻译 YAML 中的所有字符串节点
func translateYAML(node *yaml.Node, targetLang string) error {
// 如果是 DocumentNode,递归处理它的子节点
if node.Kind == yaml.DocumentNode {
for _, docNode := range node.Content {
err := translateYAML(docNode, targetLang)
if err != nil {
return err
}
}
return nil
}
// 如果是 MappingNode(字典),其子节点的奇数索引是键,偶数索引是值
if node.Kind == yaml.MappingNode {
for i := 0; i < len(node.Content); i += 2 {
//keyNode := node.Content[i] // 这是键节点
valueNode := node.Content[i+1] // 这是值节点
// 只翻译值节点
if valueNode.Kind == yaml.ScalarNode && valueNode.Tag == "!!str" {
transText, err := translateText(valueNode.Value, targetLang)
if err != nil {
return err
}
valueNode.Value = transText
} else {
// 递归处理嵌套结构
err := translateYAML(valueNode, targetLang)
if err != nil {
return err
}
}
}
} else if node.Kind == yaml.SequenceNode { // 如果是数组,递归翻译每个元素
for _, elem := range node.Content {
err := translateYAML(elem, targetLang)
if err != nil {
return err
}
}
} else if node.Kind == yaml.ScalarNode && node.Tag == "!!str" { // 如果是简单的字符串值,直接翻译
transText, err := translateText(node.Value, targetLang)
if err != nil {
return err
}
node.Value = transText
}
return nil
}
该函数是工具的核心部分,负责递归遍历并翻译 YAML
文件中的所有字符串节点。它递归处理 yaml.Node
对象的每一个节点,包括字典、数组以及字符串值:
YAML
文档的根节点,函数会递归处理根节点的所有子节点,确保整个 YAML
文件都被遍历到。MappingNode
),代码会遍历键值对并尝试翻译值部分。每对键值中,奇数索引是键,偶数索引是值。因此,函数跳过键节点,只翻译字符串类型的值节点。SequenceNode
),函数会递归翻译数组中的每个元素,确保所有数组项中的字符串都被翻译。ScalarNode
),并且是字符串类型(!!str
标签),函数将直接调用翻译 API
将其值进行翻译。func translateText(text, targetLang string) (string, error) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := http.Client{
Timeout: time.Minute,
Transport: tr,
}
request := struct {
TargetLang string `json:"target_lang"`
Text []string `json:"text"`
}{
TargetLang: targetLang,
Text: []string{text},
}
data, err := json.Marshal(request)
if err != nil {
return "", err
}
req, err := http.NewRequest(http.MethodPost, "https://api-free.deepl.com/v2/translate", bytes.NewReader(data))
if err != nil {
return "", err
}
// 这里需要填写你的 API 密钥
req.Header.Set("Authorization", "DeepL-Auth-Key ${替换成你的 API 密钥}")
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
errorRes := struct {
Message string `json:"message"`
}{}
err := respHandler(resp, &errorRes)
if err != nil {
return "", err
}
fmt.Printf("error: %v\n", errorRes)
return "", errors.New(strconv.Itoa(resp.StatusCode) + ": " + errorRes.Message)
}
result := struct {
Translations []struct {
DetectedSourceLanguage string `json:"detected_source_language"`
Text string `json:"text"`
} `json:"translations"`
}{}
err = respHandler(resp, &result)
if err != nil {
return "", err
}
if len(result.Translations) == 0 {
return "", errors.New("翻译失败")
}
return result.Translations[0].Text, nil
}
func respHandler(resp *http.Response, expectedPrt any) error {
if expectedPrt == nil {
return errors.New("expected 不能为空")
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if len(bodyBytes) == 0 {
return nil
}
err = json.Unmarshal(bodyBytes, expectedPrt)
if err != nil {
return err
}
return nil
}
该函数负责通过 DeepL
翻译 API
来对文本进行翻译:
TLS
配置的 HTTP
客户端,防止请求长时间无响应。json.Marshal
将请求数据(目标语言和待翻译文本)序列化为 JSON
格式,然后构造一个 HTTP POST
请求,并设置 Authorization
头部字段,填入 DeepL
的 API
密钥。HTTP
请求并等待响应。请求的 URL
是 DeepL
的翻译 API
地址。200 OK
,则解析 JSON
响应体,提取翻译后的文本并返回给调用方。如果发生错误(如请求失败或返回错误信息),则返回相应的错误提示。func main() {
// 定义命令行参数
inputFile := flag.String("input", "", "输入 YAML 文件路径")
outputFile := flag.String("output", "", "输出 YAML 文件路径")
targetLang := flag.String("lang", "", "目标语言代码")
// 解析命令行参数
flag.Parse()
// 检查必要的参数
if *inputFile == "" || *outputFile == "" {
fmt.Println("必须指定输入和输出文件路径")
flag.Usage()
os.Exit(1)
}
// 读取 YAML 文件
yamlData, err := readYAML(*inputFile)
if err != nil {
fmt.Println("读取 YAML 文件错误:", err)
return
}
// 翻译 YAML 内容
err = translateYAML(yamlData, *targetLang)
if err != nil {
fmt.Println("翻译 YAML 内容时出错:", err)
return
}
// 写回翻译后的 YAML 文件
err = writeYAML(*outputFile, yamlData)
if err != nil {
fmt.Println("写入 YAML 文件时出错:", err)
return
}
fmt.Println("翻译完成,已写入", *outputFile)
}
main
函数是程序的入口,通过命令行参数指定输入文件、输出文件和目标语言。核心流程:
flag.String
定义并解析命令行参数,包括输入文件路径、输出文件路径和目标语言。readYAML
函数读取并解析输入文件,然后调用 translateYAML
函数递归翻译文件中的字符串,最后调用 writeYAML
将翻译后的数据写回输出文件。package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
"gopkg.in/yaml.v3"
)
func translateText(text, targetLang string) (string, error) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := http.Client{
Timeout: time.Minute,
Transport: tr,
}
request := struct {
TargetLang string `json:"target_lang"`
Text []string `json:"text"`
}{
TargetLang: targetLang,
Text: []string{text},
}
data, err := json.Marshal(request)
if err != nil {
return "", err
}
req, err := http.NewRequest(http.MethodPost, "https://api-free.deepl.com/v2/translate", bytes.NewReader(data))
if err != nil {
return "", err
}
// 这里需要填写你的 API 密钥
req.Header.Set("Authorization", "DeepL-Auth-Key ${替换成你的 API 密钥}")
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
errorRes := struct {
Message string `json:"message"`
}{}
err := respHandler(resp, &errorRes)
if err != nil {
return "", err
}
fmt.Printf("error: %v\n", errorRes)
return "", errors.New(strconv.Itoa(resp.StatusCode) + ": " + errorRes.Message)
}
result := struct {
Translations []struct {
DetectedSourceLanguage string `json:"detected_source_language"`
Text string `json:"text"`
} `json:"translations"`
}{}
err = respHandler(resp, &result)
if err != nil {
return "", err
}
if len(result.Translations) == 0 {
return "", errors.New("翻译失败")
}
return result.Translations[0].Text, nil
}
func respHandler(resp *http.Response, expectedPrt any) error {
if expectedPrt == nil {
return errors.New("expected 不能为空")
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if len(bodyBytes) == 0 {
return nil
}
err = json.Unmarshal(bodyBytes, expectedPrt)
if err != nil {
return err
}
return nil
}
// 递归遍历并翻译 YAML 中的所有字符串节点
func translateYAML(node *yaml.Node, targetLang string) error {
// 如果是 DocumentNode,递归处理它的子节点
if node.Kind == yaml.DocumentNode {
for _, docNode := range node.Content {
err := translateYAML(docNode, targetLang)
if err != nil {
return err
}
}
return nil
}
// 如果是 MappingNode(字典),其子节点的奇数索引是键,偶数索引是值
if node.Kind == yaml.MappingNode {
for i := 0; i < len(node.Content); i += 2 {
//keyNode := node.Content[i] // 这是键节点
valueNode := node.Content[i+1] // 这是值节点
// 只翻译值节点
if valueNode.Kind == yaml.ScalarNode && valueNode.Tag == "!!str" {
transText, err := translateText(valueNode.Value, targetLang)
if err != nil {
return err
}
valueNode.Value = transText
} else {
// 递归处理嵌套结构
err := translateYAML(valueNode, targetLang)
if err != nil {
return err
}
}
}
} else if node.Kind == yaml.SequenceNode { // 如果是数组,递归翻译每个元素
for _, elem := range node.Content {
err := translateYAML(elem, targetLang)
if err != nil {
return err
}
}
} else if node.Kind == yaml.ScalarNode && node.Tag == "!!str" { // 如果是简单的字符串值,直接翻译
transText, err := translateText(node.Value, targetLang)
if err != nil {
return err
}
node.Value = transText
}
return nil
}
// 读取 YAML 文件
func readYAML(filename string) (*yaml.Node, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var yamlData yaml.Node
err = yaml.Unmarshal(data, &yamlData)
if err != nil {
return nil, err
}
return &yamlData, nil
}
// 写入 YAML 文件
func writeYAML(filename string, node *yaml.Node) error {
data, err := yaml.Marshal(node)
if err != nil {
return err
}
return os.WriteFile(filename, data, 0644)
}
func main() {
// 定义命令行参数
inputFile := flag.String("input", "", "输入 YAML 文件路径")
outputFile := flag.String("output", "", "输出 YAML 文件路径")
targetLang := flag.String("lang", "", "目标语言代码")
// 解析命令行参数
flag.Parse()
// 检查必要的参数
if *inputFile == "" || *outputFile == "" {
fmt.Println("必须指定输入和输出文件路径")
flag.Usage()
os.Exit(1)
}
// 读取 YAML 文件
yamlData, err := readYAML(*inputFile)
if err != nil {
fmt.Println("读取 YAML 文件错误:", err)
return
}
// 翻译 YAML 内容
err = translateYAML(yamlData, *targetLang)
if err != nil {
fmt.Println("翻译 YAML 内容时出错:", err)
return
}
// 写回翻译后的 YAML 文件
err = writeYAML(*outputFile, yamlData)
if err != nil {
fmt.Println("写入 YAML 文件时出错:", err)
return
}
fmt.Println("翻译完成,已写入", *outputFile)
}
待翻译的 input.yaml
文件:
a: 早上好
b: 下午好
c: 晚上好
d: 1234
e:
- 程序员
- 陈明勇
f:
g: 开发者
运行程序,对 YAML
文件内容进行翻译:
go run main.go -input input.yaml -output output.yaml -lang EN
翻译后的 YAML
文件:
a: Good morning!
b: good afternoon
c: Good evening!
d: 1234
e:
- programmer
- Chen Mingyong (1937-), Chinese-American physicist
f:
g: developers
整体来说,代码通过递归的方式处理 YAML
数据,使用 DeepL API
对 YAML
中的文本进行翻译。开发这个工具需要注意的一些问题:
flag
模块,我们可以从命令行参数中获取到输入文件、输出文件和目标语言的值,确保工具的灵活性和可配置性,方便在不同的场景中使用。YAML
文件通常包含复杂的层级结构,可能有字典、数组和嵌套的对象。因此,代码中的 translateYAML
函数采用了递归方式来处理每个节点,不论是简单的字符串、数组还是嵌套的结构,都能够自动遍历并翻译其中的文本节点。map
结构接收 YAML
文件的内容:map[any]any
这种数据结构的键是无序的。虽然 Go
的 map
是非常高效的数据结构,但它并不能保证键值对的顺序,这意味着当你解析和重新生成 YAML
文件时,会导致字段顺序不同于原文件。幸运的是,gopkg.in/yaml.v3
版本提供了保持顺序的功能。yaml.Node
是 yaml.v3
提供的一个更灵活的数据结构,它可以保持节点的顺序。因此,我们可以使用 yaml.Node
作为数据结构来保持顺序。translateText
函数,调用 DeepL API
来进行文本翻译。需要注意的地方:
1、合理设置 API
请求的超时时间,以应对网络延迟或 API
响应慢的情况。
2、处理 API
响应时,需要考虑到响应的正确性和错误处理(如 API
调用失败或返回空的翻译结果)。你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。
成功的路上并不拥挤,有没有兴趣结个伴?
关注我,加我好友,一起学习一起进步!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。