modpack_localizer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../util/indent_helper"
4
+ require_relative "title_extractor"
5
+ require_relative "subtitle_extractor"
6
+ require_relative "description_extractor"
7
+
8
+ module ModpackLocalizer
9
+ module SNBT
10
+ # .snbtファイルからタイトル、サブタイトル、説明を抽出するクラス
11
+ class Reader
12
+ include TitleExtractor
13
+ include SubtitleExtractor
14
+ include DescriptionExtractor
15
+
16
+ DESC_START_LENGTH = 14
17
+ DESC_END_LENGTH = -2
18
+
19
+ # @param [String] file_path ファイルのパス
20
+ # @return [ModpackLocalizer::SNBT::Reader]
21
+ def initialize(file_path)
22
+ @file_path = file_path
23
+ end
24
+
25
+ # タイトル、サブタイトル、説明を抽出する
26
+ #
27
+ # @return [Array<Array<Hash>>] タイトル、サブタイトル、説明の配列
28
+ def extract_all
29
+ titles = extract_titles
30
+ subtitles = extract_subtitles
31
+ descriptions = extract_descriptions
32
+
33
+ [titles, subtitles, descriptions]
34
+ end
35
+
36
+ # タイトルを抽出する
37
+ #
38
+ # @return [Array<Hash>] タイトル、行番号、インデントの配列
39
+ def extract_titles
40
+ super(@file_path)
41
+ end
42
+
43
+ # サブタイトルを抽出する
44
+ #
45
+ # @return [Array<Hash>] サブタイトル、行番号、インデントの配列
46
+ def extract_subtitles
47
+ super(@file_path)
48
+ end
49
+
50
+ # 説明を抽出する
51
+ #
52
+ # @return [Array<Hash>] 説明、開始行、終了行、インデントの配列
53
+ def extract_descriptions
54
+ super(@file_path)
55
+ end
56
+
57
+ # title: "some title"のような形式から、"some title"を抽出する
58
+ # 説明の場合は、description: ["some description"]のような形式から、"some description"を抽出する
59
+ #
60
+ # @param [String] line 行
61
+ # @param [Boolean] is_desc 説明かどうか
62
+ # @return [String] タイトル or サブタイトル or 説明
63
+ def extract_oneline(line, is_desc: false)
64
+ stripped_line = line.strip
65
+ return stripped_line.split(":", 2)[1] unless is_desc
66
+
67
+ if oneline_description?(line)
68
+ stripped_line[DESC_START_LENGTH..DESC_END_LENGTH]
69
+ elsif start_of?(line, key: :description)
70
+ stripped_line.split("[", 2)[1]
71
+ else
72
+ stripped_line.split("]", 2)[0]
73
+ end
74
+ end
75
+
76
+ # 1行の説明かどうか
77
+ #
78
+ # @param [String] line 行
79
+ # @return [Boolean]
80
+ def oneline_description?(line)
81
+ stripped_line = line.strip
82
+ start_of?(line, key: :description) && stripped_line.end_with?("]")
83
+ end
84
+
85
+ # どのコンテンツの開始行か
86
+ #
87
+ # @param [String] line 行
88
+ # @param [Symbol] key コンテンツの種類
89
+ # @return [Boolean]
90
+ def start_of?(line, key:)
91
+ stripped_line = line.strip
92
+ sections = {
93
+ title: "title:",
94
+ subtitle: "subtitle:",
95
+ description: "description: ["
96
+ }
97
+
98
+ stripped_line.start_with?(sections[key])
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "../util/indent_helper"
2
+
3
+ module ModpackLocalizer
4
+ module SNBT
5
+ # SNBT形式のファイルからsubtitle: "some subtitle"を抽出するモジュール
6
+ module SubtitleExtractor
7
+ include IndentHelper
8
+
9
+ # subtitle: "some subtitle"を抽出する
10
+ #
11
+ # @param [String] file_path ファイルのパス
12
+ # @return [Array<Hash>] サブタイトルと行番号の配列
13
+ def extract_subtitles(file_path)
14
+ subtitles = []
15
+ lines = File.readlines(file_path)
16
+ lines.each_with_index do |line, index|
17
+ next unless start_of?(line, key: :subtitle)
18
+
19
+ subtitles << {
20
+ type: :subtitle,
21
+ text: extract_oneline(line),
22
+ start_line: index,
23
+ end_line: index,
24
+ indent: count_indent(line)
25
+ }
26
+ end
27
+
28
+ subtitles
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "../util/indent_helper"
2
+
3
+ module ModpackLocalizer
4
+ module SNBT
5
+ # SNBT形式のファイルからtitle: "some title"を抽出するモジュール
6
+ module TitleExtractor
7
+ include IndentHelper
8
+
9
+ # title: "some title"を抽出する
10
+ #
11
+ # @param [String] file_path ファイルのパス
12
+ # @return [Array<Hash>] タイトルと行番号の配列
13
+ def extract_titles(file_path)
14
+ titles = []
15
+ lines = File.readlines(file_path)
16
+ lines.each_with_index do |line, index|
17
+ next unless start_of?(line, key: :title)
18
+
19
+ titles << {
20
+ type: :title,
21
+ text: extract_oneline(line),
22
+ start_line: index,
23
+ end_line: index,
24
+ indent: count_indent(line)
25
+ }
26
+ end
27
+
28
+ titles
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,74 @@
1
+ require_relative "../util/error"
2
+ require_relative "../util/indent_helper"
3
+ require_relative "formatter"
4
+
5
+ module ModpackLocalizer
6
+ module SNBT
7
+ # .snbtファイルの翻訳された内容を整形して出力するクラス
8
+ class Writer
9
+ include IndentHelper
10
+
11
+ # @param [String] file_path ファイルのパス
12
+ # @return [ModpackLocalizer::SNBT::Writer]
13
+ def initialize(file_path)
14
+ @input_file_path = file_path
15
+ # questsとすると、quests.snbtも変換されてしまうので、quests/とする
16
+ @output_file_path = file_path.gsub("quests/", "output/quests/")
17
+ @formatter = ModpackLocalizer::SNBT::Formatter.new
18
+ end
19
+
20
+ # 翻訳された内容でoutput_file_pathを上書きする
21
+ #
22
+ # @param [Hash] translated_contents 翻訳された内容
23
+ # @return [void]
24
+ def overwrites(translated_contents)
25
+ # ディレクトリが存在しない場合は作成
26
+ FileUtils.mkdir_p(File.dirname(@output_file_path))
27
+
28
+ # 一度上書きしている場合は上書き後のファイルを読み込む
29
+ # 常に上書き前のファイルを読み込むと、前回の上書きが消えてしまう
30
+ lines = File.readlines(first_overwrite? ? @input_file_path : @output_file_path)
31
+ formatted_lines = overwrite_lines(lines, translated_contents)
32
+
33
+ File.open(@output_file_path, "w") do |file|
34
+ file.puts formatted_lines
35
+ end
36
+
37
+ handle_line_count_error(@output_file_path, lines.length)
38
+ end
39
+
40
+ private
41
+
42
+ # 行を上書きする
43
+ #
44
+ # @param [Array<String>] lines 行
45
+ # @param [Hash] content コンテンツ
46
+ # @return [Array<String>] 上書きされた行
47
+ def overwrite_lines(lines, content)
48
+ indent = create_indent(content[:indent])
49
+ overwritable_lines = @formatter.format_overwritable_lines(content, indent)
50
+ lines[content[:start_line]..content[:end_line]] = overwritable_lines.split("\n")
51
+ lines
52
+ end
53
+
54
+ # 最初の上書きかどうか
55
+ #
56
+ # @return [Boolean] 最初の上書きかどうか
57
+ def first_overwrite?
58
+ !File.exist?(@output_file_path)
59
+ end
60
+
61
+ # 翻訳前と翻訳後の行数が異なる場合はエラーを発生させる
62
+ #
63
+ # @param [String] output_file_path 出力ファイルのパス
64
+ # @param [Integer] before_line_count 翻訳前の行数
65
+ # @return [void]
66
+ def handle_line_count_error(output_file_path, before_line_count)
67
+ after_line_count = File.readlines(output_file_path).length
68
+ return if before_line_count == after_line_count
69
+
70
+ raise ModpackLocalizer::InvalidLineCountError.new(before_line_count, after_line_count)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,47 @@
1
+ module ModpackLocalizer
2
+ # 基底のエラークラス
3
+ class Error < StandardError; end
4
+
5
+ # パスが見つからない場合のエラークラス
6
+ class PathNotFoundError < Error
7
+ # ヒアドキュメントを使うと、先頭と末尾に空行が入る
8
+ # エラーメッセージも変更しやすいのでこうしている
9
+ DOES_NOT_EXIST = <<~TEXT.freeze
10
+ \n
11
+ Path does not exist: %s
12
+ TEXT
13
+
14
+ # @param [String] path パス
15
+ # @return [ModpackLocalizer::PathNotFoundError]
16
+ def initialize(path)
17
+ super(format(DOES_NOT_EXIST, path))
18
+ end
19
+ end
20
+
21
+ # 行数が異なる場合のエラークラス
22
+ class InvalidLineCountError < Error
23
+ INVALID_LINE_COUNT = <<~TEXT.freeze
24
+ \n
25
+ Invalid line count: %s
26
+ TEXT
27
+
28
+ # @param [Integer] expect_count 期待する行数
29
+ # @param [Integer] actual_count 実際の行数
30
+ # @return [ModpackLocalizer::InvalidLineCountError]
31
+ def initialize(expect_count, actual_count)
32
+ super(format(INVALID_LINE_COUNT, "Expected: #{expect_count}, Actual: #{actual_count}"))
33
+ end
34
+ end
35
+
36
+ class InvalidRegionCodeError < Error
37
+ INVALID_LOCALE_CODE = <<~TEXT.freeze
38
+ \n
39
+ %s is an invalid region code.
40
+ Please specify a valid language or country or region code.
41
+ TEXT
42
+
43
+ def initialize(locale_code)
44
+ super(format(INVALID_LOCALE_CODE, locale_code))
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,156 @@
1
+ require "rainbow"
2
+
3
+ module ModpackLocalizer
4
+ # ModpackLocalizer gemについてのヘルプを表示するクラス
5
+ class Help
6
+ # ModpackLocalizer gemについてのヘルプを表示する
7
+ #
8
+ # @return [void]
9
+ def self.help
10
+ puts <<~HELP
11
+ #{cyan("=== ModpackLocalizer Help ==========================================================").bold}\n
12
+ #{help_warning}
13
+ #{help_intro}
14
+ #{help_steps}
15
+ #{help_init_options}
16
+ #{help_information}
17
+ #{cyan("====================================================================================").bold}
18
+ HELP
19
+ end
20
+
21
+ # 警告
22
+ #
23
+ # @return [String]
24
+ def self.help_warning
25
+ <<~WARNING
26
+ #{red("Warning:").bold} Please be careful your OpenAI API usage cost.
27
+ WARNING
28
+ end
29
+
30
+ # gemの説明
31
+ #
32
+ # @return [String]
33
+ def self.help_intro
34
+ <<~INTRO
35
+ #{cyan("Introduction:").bold}
36
+ Translator for #{green(".snbt")} and #{green(".jar")} files.
37
+ If you want to translate to other languages than Japanese,
38
+ please add the #{green("language")} option during initialization.
39
+ Example:
40
+ #{green("ModpackLocalizer::SNBT::Performer.new(language: \"English\")")}
41
+ #{green("ModpackLocalizer::JAR::Performer.new(language: \"English\")")}
42
+ or if no specific configs required
43
+ #{green("ModpackLocalizer.omakase(language: \"English\")")}
44
+ INTRO
45
+ end
46
+
47
+ # 使用手順
48
+ #
49
+ # @return [String]
50
+ def self.help_steps
51
+ <<~STEPS
52
+ #{cyan("Steps:").bold}
53
+ 1. exec #{green("touch .env")} bash command
54
+ 2. Add #{green("OPENAI_API_KEY=your_api_key")} to .env
55
+ 3. Optional: Add #{green("OPENAI_MODEL=some_openai_model")} to .env (default: gpt-4o-mini)
56
+ 4. Add "quests" directory to your project
57
+ 5. #{green("gem install modpack_localizer")} or #{green("gem \"modpack_localizer\"")}
58
+ 6. Add #{green("require \"modpack_localizer\"")}
59
+ 7. Add #{green("modpack_localizer = ModpackLocalizer::SNBT::Performer.new")}
60
+ 8. #{green("modpack_localizer.perform(\"file_path\")")} or
61
+ #{green("modpack_localizer.perform_directory(dir_path: \"dir_path\")")}
62
+ 9. Check "output" directory
63
+ STEPS
64
+ end
65
+
66
+ # newメソッドのオプション
67
+ #
68
+ # @return [String]
69
+ def self.help_init_options
70
+ <<~OPTIONS
71
+ #{cyan("ModpackLocalizer.omakase Options:").bold}
72
+ language:
73
+ Which language do you want to translate to?
74
+ (default: Japanese)
75
+ country:
76
+ Your country name
77
+ (default: Japan)
78
+ locale_code:
79
+ Which locale code do you want to use?
80
+ If you specified this, you don't need to specify the country.
81
+ (default: nil)
82
+ threadable:
83
+ Do you want to exec in parallel?
84
+ (default: false)
85
+ #{cyan("Initialize Options:").bold}
86
+ output_logs:
87
+ Want to output OpenAI usage logs?
88
+ (default: true)
89
+ except_words:
90
+ Words that you don't want to translate
91
+ (default: empty array)
92
+ language:
93
+ Which language do you want to translate to?
94
+ (default: Japanese)
95
+ display_help:
96
+ Want to display help?
97
+ (default: true)
98
+ Only for jar performer:
99
+ country:
100
+ Your country name
101
+ (default: Japan)
102
+ locale_code:
103
+ Which locale code do you want to use?
104
+ If you specified this, you don't need to specify the country.
105
+ (default: nil)
106
+ OPTIONS
107
+ end
108
+
109
+ # その他gemに関する情報
110
+ #
111
+ # @return [String]
112
+ def self.help_information
113
+ <<~INFORMATION
114
+ #{cyan("Information:").bold}
115
+ modpack_localizer:
116
+ #{link("https://github.com/milkeclair/modpack_localizer")}
117
+ current version: #{ModpackLocalizer::VERSION}
118
+ translator:
119
+ #{link("https://github.com/milkeclair/jp_translator_from_gpt")}
120
+ current version: #{JpTranslatorFromGpt::VERSION}
121
+ INFORMATION
122
+ end
123
+
124
+ # 出力を青色にする
125
+ #
126
+ # @param [String] str
127
+ # @return [String]
128
+ def self.cyan(str)
129
+ Rainbow(str).cyan
130
+ end
131
+
132
+ # 出力を緑色にする
133
+ #
134
+ # @param [String] str
135
+ # @return [String]
136
+ def self.green(str)
137
+ Rainbow(str).green
138
+ end
139
+
140
+ # 出力を赤色にする
141
+ #
142
+ # @param [String] str
143
+ # @return [String]
144
+ def self.red(str)
145
+ Rainbow(str).red
146
+ end
147
+
148
+ # 出力をリンクのスタイルにする
149
+ #
150
+ # @param [String] str
151
+ # @return [String]
152
+ def self.link(str)
153
+ Rainbow(str).underline.bright
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,28 @@
1
+ module ModpackLocalizer
2
+ # インデントを扱うモジュール
3
+ module IndentHelper
4
+ # インデントを数える
5
+ #
6
+ # @param [String] unstripped_line stripされていない行
7
+ # @return [Integer] インデントの数
8
+ def count_indent(unstripped_line)
9
+ unstripped_line.length - unstripped_line.lstrip.length
10
+ end
11
+
12
+ # インデントを作成
13
+ #
14
+ # @param [Integer] indent インデント数
15
+ # @return [String] インデント
16
+ def create_indent(indent)
17
+ " " * indent
18
+ end
19
+
20
+ # 中間行のインデントを作成
21
+ #
22
+ # @param [Integer] indent インデント数
23
+ # @return [String] 中間行のインデント
24
+ def middle_indent(indent)
25
+ " " * (indent + 1)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModpackLocalizer
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby-progressbar"
4
+ require_relative "modpack_localizer/util/version"
5
+ require_relative "modpack_localizer/util/help"
6
+ require_relative "modpack_localizer/snbt/performer"
7
+ require_relative "modpack_localizer/jar/performer"
8
+
9
+ # SNBT形式のファイルを翻訳する
10
+ # 翻訳できるプロパティ
11
+ # - title
12
+ # - subtitle
13
+ # - description
14
+ module ModpackLocalizer
15
+ # quests, mods配下のファイルを全て翻訳する
16
+ # locale_codeを指定する場合、countryの指定は不要
17
+ #
18
+ # @param [String] language 言語
19
+ # @param [String] country 国
20
+ # @param [String] locale_code ロケールコード(例: "ja_jp")
21
+ # @param [Boolean] threadable quests, modsの翻訳を並列で行うか
22
+ # @return [void]
23
+ def self.omakase(language: "Japanese", country: "Japan", locale_code: nil, threadable: false)
24
+ performers = [] << ModpackLocalizer::SNBT::Performer.new(language: language)
25
+ performers << ModpackLocalizer::JAR::Performer.new(
26
+ language: language, country: country, locale_code: locale_code, display_help: false
27
+ )
28
+
29
+ if threadable
30
+ threads = performers.map { |pfm| Thread.new { pfm.perform_directory(loggable: false) } }
31
+ threads.each(&:join)
32
+ else
33
+ performers.each(&:perform_directory)
34
+ end
35
+ end
36
+
37
+ # ModpackLocalizer gemについてのヘルプを表示する
38
+ #
39
+ # @return [void]
40
+ def self.help
41
+ ModpackLocalizer::Help.help
42
+ end
43
+
44
+ # プログレスバーを生成する
45
+ #
46
+ # @param [String] file_path ファイルのパス
47
+ # @param [Integer] total プログレスバーの合計数
48
+ # @return [ProgressBar::Base] プログレスバー
49
+ def self.create_progress_bar(file_path, total)
50
+ # パスの内、カレントディレクトリ配下のパス以外は邪魔なので削除
51
+ # 例: /Users/user/quests/some.snbt -> /quests/some.snbt
52
+ puts "\nFile path: #{file_path.gsub(Dir.pwd, "")}"
53
+
54
+ ProgressBar.create(
55
+ title: "Translating...",
56
+ total: total,
57
+ progress_mark: "#",
58
+ format: "%t [%B]",
59
+ length: 80,
60
+ projector: {
61
+ type: "smoothed",
62
+ strength: 0.1
63
+ }
64
+ )
65
+ end
66
+ end
data/rake_helper.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RakeHelper
4
+ GITHUB_PACKAGES_PUSH_COMMAND =
5
+ "gem push --key github --host https://rubygems.pkg.github.com/milkeclair " \
6
+ "pkg/modpack_localizer-#{ModpackLocalizer::VERSION}.gem".freeze
7
+
8
+ RUBYGEMS_PUSH_COMMAND =
9
+ "gem push --host https://rubygems.org " \
10
+ "pkg/modpack_localizer-#{ModpackLocalizer::VERSION}.gem".freeze
11
+
12
+ def self.init_rake_tasks
13
+ RSpec::Core::RakeTask.new(:spec) { |task| task.verbose = false }
14
+ RuboCop::RakeTask.new
15
+ YARD::Rake::YardocTask.new
16
+ end
17
+
18
+ def self.build_gem
19
+ abort("gemのビルドに失敗しました") unless system("rake build")
20
+ end
21
+
22
+ def self.push_to_github_packages
23
+ abort("githubへのgemのpushに失敗しました") unless system(GITHUB_PACKAGES_PUSH_COMMAND)
24
+ end
25
+
26
+ def self.push_to_rubygems
27
+ abort("rubygemsへのgemのpushに失敗しました") unless system(RUBYGEMS_PUSH_COMMAND)
28
+ end
29
+ end
data/sig/jp_quest.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module ModpackLocalizer
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end