前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 Go 开发一个简单的 YAML 文件翻译小工具

使用 Go 开发一个简单的 YAML 文件翻译小工具

原创
作者头像
陈明勇
发布2024-10-18 19:14:38
1220
发布2024-10-18 19:14:38
举报
文章被收录于专栏:Go 技术Go技术干货

前言

有时我们需要翻译 YAML 文件的内容,但目前缺乏一个专门针对 YAML 的便捷翻译工具。为此,我们可以自己开发一个定制化的工具。本文将介绍如何使用 Go 语言开发一个简单的 YAML 文件翻译工具。

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

翻译 API 选型

本文选择 deepl 提供的翻译 API 进行演示开发,因为即使你是个人开发者,也能免费享受到 deepl 每月提供的限量的文本翻译额度。

工具代码编写

读取 YAML 文件函数

代码语言:go
复制
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 数据:使用 yaml.Unmarshal 方法将读取的字节数据解析成 yaml.Node 对象。yaml.NodeGo YAML 包中的一种抽象数据结构,可以存储复杂的 YAML 层次结构,适合需要遍历和修改节点的情况。

写入 YAML 文件函数

代码语言:go
复制
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 格式,并写回到文件中:

  1. 序列化 YAML 数据:首先使用 yaml.Marshal 将内存中的 yaml.Node 数据结构转换成字节数组。这是一个将 YAML 数据对象转换回可读文件格式的步骤。
  2. 写入文件:使用 os.WriteFile 将序列化后的字节数组写入指定的文件路径,参数 0644 设置了文件的权限,表示文件所有者有读写权限,而其他用户只有读取权限。

YAML 翻译函数

代码语言:go
复制
// 递归遍历并翻译 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 对象的每一个节点,包括字典、数组以及字符串值:

  1. DocumentNode:这是 YAML 文档的根节点,函数会递归处理根节点的所有子节点,确保整个 YAML 文件都被遍历到。
  2. MappingNode:如果是字典结构(MappingNode),代码会遍历键值对并尝试翻译值部分。每对键值中,奇数索引是键,偶数索引是值。因此,函数跳过键节点,只翻译字符串类型的值节点。
  3. SequenceNode:如果是数组结构(SequenceNode),函数会递归翻译数组中的每个元素,确保所有数组项中的字符串都被翻译。
  4. ScalarNode:如果节点是标量值(ScalarNode),并且是字符串类型(!!str 标签),函数将直接调用翻译 API 将其值进行翻译。

调用 API 翻译文本函数

代码语言:go
复制
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 来对文本进行翻译:

  1. 构建 HTTP 客户端:为了保证请求的可靠性,函数创建了一个带有超时时间和 TLS 配置的 HTTP 客户端,防止请求长时间无响应。
  2. 构建请求体:函数使用 json.Marshal 将请求数据(目标语言和待翻译文本)序列化为 JSON 格式,然后构造一个 HTTP POST 请求,并设置 Authorization 头部字段,填入 DeepLAPI 密钥。
  3. 发送请求:函数发送 HTTP 请求并等待响应。请求的 URLDeepL 的翻译 API 地址。
  4. 处理响应:如果响应状态码是 200 OK,则解析 JSON 响应体,提取翻译后的文本并返回给调用方。如果发生错误(如请求失败或返回错误信息),则返回相应的错误提示。

入口函数

代码语言:go
复制
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 将翻译后的数据写回输出文件。

完整代码

代码语言:go
复制
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 文件:

代码语言:yaml
复制
a: 早上好
b: 下午好
c: 晚上好
d: 1234
e:
  - 程序员
  - 陈明勇
f:
  g: 开发者

运行程序,对 YAML 文件内容进行翻译:

代码语言:bash
复制
go run main.go -input input.yaml -output output.yaml -lang EN

翻译后的 YAML 文件:

代码语言: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 APIYAML 中的文本进行翻译。开发这个工具需要注意的一些问题:

  • 动态获取参数:通过使用 flag 模块,我们可以从命令行参数中获取到输入文件、输出文件和目标语言的值,确保工具的灵活性和可配置性,方便在不同的场景中使用。
  • 递归处理结构化数据YAML 文件通常包含复杂的层级结构,可能有字典、数组和嵌套的对象。因此,代码中的 translateYAML 函数采用了递归方式来处理每个节点,不论是简单的字符串、数组还是嵌套的结构,都能够自动遍历并翻译其中的文本节点。
  • 避免使用 map 结构接收 YAML 文件的内容map[any]any 这种数据结构的键是无序的。虽然 Gomap 是非常高效的数据结构,但它并不能保证键值对的顺序,这意味着当你解析和重新生成 YAML 文件时,会导致字段顺序不同于原文件。幸运的是,gopkg.in/yaml.v3 版本提供了保持顺序的功能。yaml.Nodeyaml.v3 提供的一个更灵活的数据结构,它可以保持节点的顺序。因此,我们可以使用 yaml.Node 作为数据结构来保持顺序。
  • 翻译 API 的使用:通过 translateText 函数,调用 DeepL API 来进行文本翻译。需要注意的地方: 1、合理设置 API 请求的超时时间,以应对网络延迟或 API 响应慢的情况。 2、处理 API 响应时,需要考虑到响应的正确性和错误处理(如 API 调用失败或返回空的翻译结果)。

你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。

成功的路上并不拥挤,有没有兴趣结个伴?

关注我,加我好友,一起学习一起进步!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 翻译 API 选型
  • 工具代码编写
    • 读取 YAML 文件函数
      • 写入 YAML 文件函数
        • YAML 翻译函数
          • 调用 API 翻译文本函数
            • 入口函数
              • 完整代码
                • 效果演示
                • 小结
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档