phraseapp_updater 0.1.7 → 2.0.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.
- checksums.yaml +5 -5
- data/Gemfile +1 -0
- data/README.markdown +79 -72
- data/bin/phraseapp_common.sh +82 -0
- data/bin/phraseapp_updater +220 -93
- data/bin/synchronize_phraseapp.sh +141 -0
- data/lib/phraseapp_updater.rb +105 -116
- data/lib/phraseapp_updater/locale_file.rb +69 -28
- data/lib/phraseapp_updater/locale_file/json_file.rb +20 -13
- data/lib/phraseapp_updater/locale_file/yaml_file.rb +16 -9
- data/lib/phraseapp_updater/phraseapp_api.rb +116 -60
- data/lib/phraseapp_updater/version.rb +3 -1
- data/phraseapp_updater.gemspec +1 -0
- metadata +19 -4
- data/lib/phraseapp_updater/locale_file/loader.rb +0 -24
@@ -0,0 +1,141 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
set -o pipefail
|
5
|
+
|
6
|
+
SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
|
7
|
+
. "${SCRIPTPATH}/phraseapp_common.sh"
|
8
|
+
|
9
|
+
if [ ! -d ".git" ]; then
|
10
|
+
echo "Error: must be run in a git checkout root" >&2
|
11
|
+
exit 1
|
12
|
+
fi
|
13
|
+
|
14
|
+
# Configuration is via environment, and expected to be provided from a Ruby
|
15
|
+
# driver.
|
16
|
+
for variable in PHRASEAPP_API_KEY PHRASEAPP_PROJECT_ID BRANCH REMOTE PREFIX FILE_FORMAT NO_COMMIT; do
|
17
|
+
if [ -z "${!variable}" ]; then
|
18
|
+
echo "Error: must specify $variable" >&2
|
19
|
+
exit 1
|
20
|
+
fi
|
21
|
+
done
|
22
|
+
|
23
|
+
# Ensure we're up to date
|
24
|
+
git fetch "${REMOTE}"
|
25
|
+
|
26
|
+
current_branch=$(git rev-parse "${REMOTE}/${BRANCH}")
|
27
|
+
|
28
|
+
# First, fetch the current contents of PhraseApp's staged ("verified") state.
|
29
|
+
current_phraseapp_path=$(make_temporary_directory)
|
30
|
+
common_ancestor=$(phraseapp_updater download "${current_phraseapp_path}" \
|
31
|
+
--phraseapp_api_key="${PHRASEAPP_API_KEY}" \
|
32
|
+
--phraseapp_project_id="${PHRASEAPP_PROJECT_ID}" \
|
33
|
+
--file_format="${FILE_FORMAT}")
|
34
|
+
|
35
|
+
# If common_ancestor is not available or reachable from BRANCH, we've been
|
36
|
+
# really naughty and rebased without uploading the result to PhraseApp
|
37
|
+
# afterwards. If it's not available, we lose: the best we can do is manually
|
38
|
+
# perform a 2-way diff and force upload to phraseapp. If it's still available
|
39
|
+
# but not reachable, we can still try and perform a 3 way merge, but the results
|
40
|
+
# will not be as accurate: warn the user.
|
41
|
+
if ! git cat-file -e "${common_ancestor}^{commit}"; then
|
42
|
+
echo "Common ancestor commit could not be found: was '${BRANCH}' rebased without updating PhraseApp?" >&2
|
43
|
+
exit 1
|
44
|
+
elif ! git merge-base --is-ancestor "${common_ancestor}" "${current_branch}"; then
|
45
|
+
echo "Warning: ancestor commit was not reachable from '${BRANCH}': 3-way merge may be inaccurate" >&2
|
46
|
+
fi
|
47
|
+
|
48
|
+
current_branch_path=$(extract_commit "${current_branch}")
|
49
|
+
common_ancestor_path=$(extract_commit "${common_ancestor}")
|
50
|
+
|
51
|
+
current_phraseapp_tree=$(make_tree_from_directory "${current_phraseapp_path}")
|
52
|
+
|
53
|
+
# We have four cases to handle:
|
54
|
+
# 1: PhraseApp and BRANCH locales both changed since common_ancestor:
|
55
|
+
# * Record current PhraseApp in a commit A with parent `common_ancestor`
|
56
|
+
# * 3-way merge BRANCH locales and PhraseApp contents
|
57
|
+
# * Commit the result to BRANCH with parents BRANCH and A, yielding B
|
58
|
+
# * Push the result to phraseapp with new common_ancestor B
|
59
|
+
# 2: Only BRANCH changed since common_ancestor:
|
60
|
+
# * Push BRANCH locales to PhraseApp with new common_ancestor BRANCH
|
61
|
+
# 3: Only PhraseApp changed since common_ancestor:
|
62
|
+
# * Commit and push PhraseApp contents to BRANCH yielding commit A
|
63
|
+
# * Update PhraseApp common_ancestor to A
|
64
|
+
# 4: Neither changed:
|
65
|
+
# * Do nothing
|
66
|
+
|
67
|
+
phraseapp_changed=$(if locales_changed "${common_ancestor_path}" "${current_phraseapp_path}"; then echo t; else echo f; fi)
|
68
|
+
branch_changed=$(if locales_changed "${common_ancestor_path}" "${current_branch_path}"; then echo t; else echo f; fi)
|
69
|
+
|
70
|
+
if [ "${phraseapp_changed}" = 't' ] && [ "${branch_changed}" = 't' ]; then
|
71
|
+
echo "$BRANCH branch and PhraseApp both changed: 3-way merging" >&2
|
72
|
+
|
73
|
+
# 3-way merge
|
74
|
+
merge_resolution_path=$(make_temporary_directory)
|
75
|
+
phraseapp_updater merge "${common_ancestor_path}" "${current_branch_path}" "${current_phraseapp_path}" \
|
76
|
+
--to "${merge_resolution_path}" \
|
77
|
+
--file_format="${FILE_FORMAT}"
|
78
|
+
|
79
|
+
if [ "$NO_COMMIT" != 't' ]; then
|
80
|
+
# Create a commit to record the pre-merge state of PhraseApp
|
81
|
+
phraseapp_commit=$(git commit-tree "${current_phraseapp_tree}" \
|
82
|
+
-p "${common_ancestor}" \
|
83
|
+
-m "Remote locale changes made on PhraseApp (drop when rebasing)")
|
84
|
+
|
85
|
+
# Commit merge result to PREFIX in BRANCH
|
86
|
+
merge_resolution_tree=$(make_tree_from_directory "${merge_resolution_path}")
|
87
|
+
merged_branch_tree=$(replace_nested_tree "${current_branch}^{tree}" "${PREFIX}" "${merge_resolution_tree}")
|
88
|
+
|
89
|
+
merge_commit=$(git commit-tree "${merged_branch_tree}" \
|
90
|
+
-p "${current_branch}" \
|
91
|
+
-p "${phraseapp_commit}" \
|
92
|
+
-m "Merged locale changes from PhraseApp")
|
93
|
+
|
94
|
+
# Push to BRANCH
|
95
|
+
git push "${REMOTE}" "${merge_commit}:refs/heads/${BRANCH}"
|
96
|
+
new_parent_commit="${merge_commit}"
|
97
|
+
else
|
98
|
+
# Merge is only to phraseapp: record current branch as new common ancestor
|
99
|
+
echo "Not committing to $BRANCH" >&2
|
100
|
+
new_parent_commit="${current_branch}"
|
101
|
+
fi
|
102
|
+
|
103
|
+
# Push merge result to phraseapp
|
104
|
+
phraseapp_updater upload "${merge_resolution_path}" \
|
105
|
+
--parent_commit="${new_parent_commit}" \
|
106
|
+
--phraseapp_api_key="${PHRASEAPP_API_KEY}" \
|
107
|
+
--phraseapp_project_id="${PHRASEAPP_PROJECT_ID}" \
|
108
|
+
--file_format="${FILE_FORMAT}"
|
109
|
+
|
110
|
+
elif [ "${branch_changed}" = 't' ]; then
|
111
|
+
echo "Only $BRANCH branch changed: updating PhraseApp" >&2
|
112
|
+
|
113
|
+
# Upload to phraseapp
|
114
|
+
phraseapp_updater upload "${current_branch_path}" \
|
115
|
+
--parent_commit="${current_branch}" \
|
116
|
+
--phraseapp_api_key="${PHRASEAPP_API_KEY}" \
|
117
|
+
--phraseapp_project_id="${PHRASEAPP_PROJECT_ID}" \
|
118
|
+
--file_format="${FILE_FORMAT}"
|
119
|
+
|
120
|
+
elif [ "${phraseapp_changed}" = 't' ]; then
|
121
|
+
if [ "$NO_COMMIT" != 't' ]; then
|
122
|
+
echo "Only PhraseApp changed: updating $BRANCH branch" >&2
|
123
|
+
|
124
|
+
updated_branch_tree=$(replace_nested_tree "${current_branch}^{tree}" "${PREFIX}" "${current_phraseapp_tree}")
|
125
|
+
update_commit=$(git commit-tree "${updated_branch_tree}" \
|
126
|
+
-p "${current_branch}" \
|
127
|
+
-m "Incorporate locale changes from PhraseApp")
|
128
|
+
|
129
|
+
git push "${REMOTE}" "${update_commit}:refs/heads/${BRANCH}"
|
130
|
+
|
131
|
+
# Set ancestor on phraseapp
|
132
|
+
phraseapp_updater update_parent_commit \
|
133
|
+
--parent_commit="${update_commit}" \
|
134
|
+
--phraseapp_api_key="${PHRASEAPP_API_KEY}" \
|
135
|
+
--phraseapp_project_id="${PHRASEAPP_PROJECT_ID}"
|
136
|
+
else
|
137
|
+
echo "Only PhraseApp changed: not committing to $BRANCH branch" >&2
|
138
|
+
fi
|
139
|
+
else
|
140
|
+
echo "No changes made since common ancestor" >&2
|
141
|
+
fi
|
data/lib/phraseapp_updater.rb
CHANGED
@@ -1,176 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'phraseapp_updater/version'
|
2
4
|
require 'phraseapp_updater/index_by'
|
3
5
|
require 'phraseapp_updater/differ'
|
4
6
|
require 'phraseapp_updater/locale_file'
|
5
|
-
require 'phraseapp_updater/locale_file/loader'
|
6
7
|
require 'phraseapp_updater/phraseapp_api'
|
7
8
|
require 'phraseapp_updater/yml_config_loader'
|
8
9
|
|
9
10
|
class PhraseAppUpdater
|
10
11
|
using IndexBy
|
11
12
|
|
12
|
-
def
|
13
|
+
def self.for_new_project(phraseapp_api_key, phraseapp_project_name, file_format, parent_commit)
|
14
|
+
api = PhraseAppAPI.new(phraseapp_api_key, nil, LocaleFile.class_for_file_format(file_format))
|
15
|
+
project_id = api.create_project(phraseapp_project_name, parent_commit)
|
16
|
+
return self.new(phraseapp_api_key, project_id, file_format), project_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(phraseapp_api_key, phraseapp_project_id, file_format, default_locale: 'en')
|
13
20
|
@locale_file_class = LocaleFile.class_for_file_format(file_format)
|
21
|
+
@default_locale = default_locale
|
14
22
|
@phraseapp_api = PhraseAppAPI.new(phraseapp_api_key, phraseapp_project_id, @locale_file_class)
|
15
23
|
end
|
16
24
|
|
17
|
-
def
|
18
|
-
|
25
|
+
def diff_directories(our_path, their_path)
|
26
|
+
(our_locales, their_locales) = load_locale_directories(our_path, their_path)
|
27
|
+
diff_locale_files(our_locales, their_locales)
|
19
28
|
end
|
20
29
|
|
21
|
-
def
|
22
|
-
|
30
|
+
def merge_directories(our_path, their_path, ancestor_path, result_path)
|
31
|
+
(our_locales, their_locales, ancestor_locales) =
|
32
|
+
load_locale_directories(our_path, their_path, ancestor_path)
|
23
33
|
|
24
|
-
|
34
|
+
merged_locales = merge_locale_files(our_locales, their_locales, ancestor_locales)
|
25
35
|
|
26
|
-
(
|
27
|
-
|
36
|
+
write_locale_directory(result_path, merged_locales)
|
37
|
+
end
|
28
38
|
|
29
|
-
|
39
|
+
def merge_files(ours, theirs, ancestor, result)
|
40
|
+
our_file, their_file = load_locale_files(ours, theirs)
|
41
|
+
# Read the ancestor if provided
|
42
|
+
ancestor_file = load_locale_file(ancestor) unless ancestor.nil?
|
30
43
|
|
31
|
-
|
32
|
-
phraseapp_files = phraseapp_files.index_by(&:name)
|
44
|
+
result_file = merge_locale_files(our_file, their_file, ancestor_file)
|
33
45
|
|
34
|
-
|
35
|
-
|
36
|
-
phraseapp_file = phraseapp_files.fetch(previous_locale_file.name)
|
46
|
+
write_locale_file(result, result_file)
|
47
|
+
end
|
37
48
|
|
38
|
-
|
39
|
-
|
40
|
-
|
49
|
+
def upload_directory(path)
|
50
|
+
locales = load_locale_directory(path)
|
51
|
+
upload_locale_files(locales)
|
52
|
+
end
|
41
53
|
|
42
|
-
|
43
|
-
|
54
|
+
def download_to_directory(path)
|
55
|
+
locale_files = download_locale_files
|
56
|
+
write_locale_directory(path, locale_files)
|
57
|
+
end
|
44
58
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
# If we do the clean up after uploading the default
|
49
|
-
# locale file, these blanks will get cleaned because
|
50
|
-
# they're not mentioned in the secondary locale files.
|
59
|
+
def update_parent_commit(parent_commit)
|
60
|
+
@phraseapp_api.update_parent_commit(parent_commit)
|
61
|
+
end
|
51
62
|
|
52
|
-
|
63
|
+
def read_parent_commit
|
64
|
+
@phraseapp_api.read_parent_commit
|
65
|
+
end
|
53
66
|
|
54
|
-
|
67
|
+
private
|
55
68
|
|
56
|
-
|
57
|
-
|
69
|
+
def diff_locale_files(our_locales, their_locales)
|
70
|
+
(our_content, their_content) = [our_locales, their_locales].map do |locales|
|
71
|
+
locales.each_with_object({}) do |locale, h|
|
72
|
+
h[locale.locale_name] = locale.parsed_content
|
73
|
+
end
|
58
74
|
end
|
59
75
|
|
60
|
-
|
61
|
-
|
76
|
+
HashDiff.diff(our_content, their_content)
|
77
|
+
end
|
62
78
|
|
63
|
-
|
64
|
-
|
79
|
+
def merge_locale_files(our_locales, their_locales, ancestor_locales)
|
80
|
+
ours = our_locales.index_by(&:locale_name)
|
81
|
+
theirs = their_locales.index_by(&:locale_name)
|
82
|
+
ancestors = ancestor_locales.index_by(&:locale_name)
|
65
83
|
|
66
|
-
|
67
|
-
@phraseapp_api.remove_keys_not_in_upload(upload_id)
|
84
|
+
locale_names = Set.new.merge(ours.keys).merge(theirs.keys)
|
68
85
|
|
69
|
-
|
86
|
+
locale_names.map do |locale_name|
|
87
|
+
our_file = ours.fetch(locale_name)
|
88
|
+
their_file = theirs.fetch(locale_name)
|
89
|
+
ancestor_file = ancestors[locale_name]
|
90
|
+
merge_locale_file(our_file, their_file, ancestor_file)
|
91
|
+
end
|
70
92
|
end
|
71
93
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
94
|
+
def upload_locale_files(locale_files)
|
95
|
+
# We assert that the default locale contains all legitimate strings, and so
|
96
|
+
# we clean up orphaned content on PhraseApp post-upload by removing keys not
|
97
|
+
# in the default locale.
|
98
|
+
default_locale_index = locale_files.find_index { |f| f.locale_name == @default_locale }
|
99
|
+
raise RuntimeError.new("Missing default locale") unless default_locale_index
|
78
100
|
|
79
|
-
|
101
|
+
upload_ids = @phraseapp_api.upload_files(locale_files, default_locale: @default_locale)
|
102
|
+
default_upload_id = upload_ids[default_locale_index]
|
80
103
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
# Clean empty strings from the data and merge the fallback data in:
|
85
|
-
# we want to replace unverified keys with their values in the fallback
|
86
|
-
phraseapp_files_without_unverified.map do |phraseapp_without_unverified_file|
|
87
|
-
without_unverified = clear_empty_strings!(phraseapp_without_unverified_file.parsed_content)
|
88
|
-
with_unverified = clear_empty_strings!(phraseapp_files_with_unverified[phraseapp_without_unverified_file.name].parsed_content)
|
89
|
-
fallback = clear_empty_strings!(fallback_files[phraseapp_without_unverified_file.name].parsed_content)
|
104
|
+
STDERR.puts "Removing keys not in default locale upload #{default_upload_id}"
|
105
|
+
@phraseapp_api.remove_keys_not_in_upload(default_upload_id)
|
106
|
+
end
|
90
107
|
|
91
|
-
|
92
|
-
|
93
|
-
|
108
|
+
def download_locale_files
|
109
|
+
known_locales = @phraseapp_api.fetch_locales
|
110
|
+
@phraseapp_api.download_files(known_locales, skip_unverified: false)
|
94
111
|
end
|
95
112
|
|
96
|
-
|
113
|
+
def merge_locale_file(our_file, their_file, ancestor_file)
|
114
|
+
if our_file.nil?
|
115
|
+
their_file
|
116
|
+
elsif their_file.nil?
|
117
|
+
our_file
|
118
|
+
else
|
119
|
+
ancestor_file ||= empty_locale_file(our_file.locale_name)
|
97
120
|
|
98
|
-
|
99
|
-
|
121
|
+
resolved_content = Differ.resolve!(
|
122
|
+
original: ancestor_file.parsed_content,
|
123
|
+
primary: our_file.parsed_content,
|
124
|
+
secondary: their_file.parsed_content)
|
100
125
|
|
101
|
-
|
102
|
-
file.name == default_locale.name
|
126
|
+
@locale_file_class.from_hash(our_file.locale_name, resolved_content)
|
103
127
|
end
|
104
128
|
end
|
105
129
|
|
106
|
-
def
|
107
|
-
@
|
130
|
+
def empty_locale_file(locale_name)
|
131
|
+
@locale_file_class.from_hash(locale_name, {})
|
108
132
|
end
|
109
133
|
|
110
|
-
def load_locale_files(*
|
111
|
-
|
112
|
-
|
113
|
-
loader.filenames(path).map { |l| loader.load(l) }
|
134
|
+
def load_locale_files(*filenames)
|
135
|
+
filenames.map do |filename|
|
136
|
+
load_locale_file(filename)
|
114
137
|
end
|
115
138
|
end
|
116
139
|
|
117
|
-
def
|
118
|
-
|
119
|
-
files.map(&:name).to_set
|
120
|
-
end
|
121
|
-
|
122
|
-
# If we don't have the exact same locales for all of the sources, we can't diff them
|
123
|
-
unless file_name_lists.uniq.size == 1
|
124
|
-
message = "Number of files differs. This tool does not yet support adding\
|
125
|
-
or removing langauges: #{file_name_lists}"
|
126
|
-
raise RuntimeError.new(message)
|
127
|
-
end
|
140
|
+
def load_local_file(filename)
|
141
|
+
@locale_file_class.load_file(filename)
|
128
142
|
end
|
129
143
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
# `without_unverified`, restore the version from `originals` to
|
134
|
-
# `without_unverified`
|
135
|
-
def restore_unverified_originals!(fallback, with_unverified, without_unverified)
|
136
|
-
fallback.each do |key, value|
|
137
|
-
with_value = with_unverified[key]
|
138
|
-
|
139
|
-
case value
|
140
|
-
when Hash
|
141
|
-
if with_value.is_a?(Hash)
|
142
|
-
without_value = (without_unverified[key] ||= {})
|
143
|
-
restore_unverified_originals!(value, with_value, without_value)
|
144
|
-
end
|
145
|
-
else
|
146
|
-
if with_value && !with_value.is_a?(Hash) && !without_unverified.has_key?(key)
|
147
|
-
without_unverified[key] = value
|
148
|
-
end
|
149
|
-
end
|
144
|
+
def load_locale_directories(*paths)
|
145
|
+
paths.map do |path|
|
146
|
+
load_locale_directory(path)
|
150
147
|
end
|
151
148
|
end
|
152
149
|
|
153
|
-
def
|
154
|
-
|
155
|
-
if value == ""
|
156
|
-
true
|
157
|
-
elsif value.is_a?(Hash)
|
158
|
-
clear_empty_strings!(value)
|
159
|
-
value.empty?
|
160
|
-
else
|
161
|
-
false
|
162
|
-
end
|
163
|
-
end
|
164
|
-
hash
|
150
|
+
def load_locale_directory(path)
|
151
|
+
@locale_file_class.load_directory(path)
|
165
152
|
end
|
166
153
|
|
167
|
-
|
168
|
-
|
154
|
+
def write_locale_file(path, locale_file)
|
155
|
+
File.write(path, locale_file.content)
|
156
|
+
end
|
169
157
|
|
170
|
-
|
171
|
-
|
172
|
-
|
158
|
+
def write_locale_directory(path, locale_files)
|
159
|
+
locale_files.each do |locale_file|
|
160
|
+
full_path = File.join(path, locale_file.filename)
|
161
|
+
File.write(full_path, locale_file.content)
|
173
162
|
end
|
163
|
+
STDERR.puts "Wrote #{locale_files.count} locale files to #{path}: #{locale_files.map(&:filename)}"
|
174
164
|
end
|
175
165
|
end
|
176
|
-
|
@@ -3,50 +3,91 @@ require "phraseapp_updater/locale_file/yaml_file"
|
|
3
3
|
|
4
4
|
class PhraseAppUpdater
|
5
5
|
class LocaleFile
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :locale_name, :content, :parsed_content
|
7
7
|
|
8
|
-
class BadFileTypeError < StandardError
|
8
|
+
class BadFileTypeError < StandardError; end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
class << self
|
11
|
+
def from_hash(locale_name, hash)
|
12
|
+
new(locale_name, hash)
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
when "json"
|
17
|
-
JSONFile
|
18
|
-
when "yml", "yaml"
|
19
|
-
YAMLFile
|
20
|
-
else
|
21
|
-
raise BadFileTypeError.new("Invalid file type: #{type}")
|
15
|
+
def from_file_content(locale_name, content)
|
16
|
+
new(locale_name, load(content))
|
22
17
|
end
|
23
|
-
end
|
24
18
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
19
|
+
def load_directory(directory)
|
20
|
+
Dir[File.join(directory, "*.#{extension}")].map do |filename|
|
21
|
+
load_file(filename)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_file(filename)
|
26
|
+
unless File.readable?(filename) && File.file?(filename)
|
27
|
+
raise RuntimeError.new("Couldn't read localization file at #{filename}")
|
28
|
+
end
|
29
|
+
|
30
|
+
locale_name = File.basename(filename).chomp(".#{extension}")
|
31
|
+
from_file_content(locale_name, File.read(filename))
|
32
|
+
end
|
33
|
+
|
34
|
+
private :new
|
35
|
+
|
36
|
+
def class_for_file_format(type)
|
37
|
+
case type.downcase
|
38
|
+
when "json"
|
39
|
+
JSONFile
|
40
|
+
when "yml", "yaml"
|
41
|
+
YAMLFile
|
42
|
+
else
|
43
|
+
raise BadFileTypeError.new("Invalid file type: #{type}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def extension
|
48
|
+
raise RuntimeError.new("Abstract method")
|
49
|
+
end
|
50
|
+
|
51
|
+
def phraseapp_type
|
52
|
+
raise RuntimeError.new("Abstract method")
|
53
|
+
end
|
54
|
+
|
55
|
+
def load(_content)
|
56
|
+
raise RuntimeError.new("Abstract method")
|
57
|
+
end
|
58
|
+
|
59
|
+
def dump(_hash)
|
60
|
+
raise RuntimeError.new("Abstract method")
|
61
|
+
end
|
31
62
|
end
|
32
63
|
|
33
64
|
def to_s
|
34
|
-
|
65
|
+
locale_name
|
35
66
|
end
|
36
67
|
|
37
|
-
def
|
38
|
-
"#{
|
68
|
+
def filename
|
69
|
+
"#{locale_name}.#{self.class.extension}"
|
39
70
|
end
|
40
71
|
|
41
72
|
private
|
42
73
|
|
43
|
-
|
44
|
-
|
74
|
+
# Expects a Ruby hash
|
75
|
+
def initialize(locale_name, parsed_content)
|
76
|
+
@locale_name = locale_name
|
77
|
+
@parsed_content = normalize_hash(parsed_content)
|
78
|
+
@content = self.class.dump(@parsed_content)
|
79
|
+
freeze
|
45
80
|
end
|
46
81
|
|
47
|
-
def
|
48
|
-
|
82
|
+
def normalize_hash(hash)
|
83
|
+
hash.keys.sort_by(&:to_s).each_with_object({}) do |key, normalized_hash|
|
84
|
+
val = hash[key]
|
85
|
+
next if val == '' || (val.is_a?(Hash) && val.empty?)
|
86
|
+
|
87
|
+
val = normalize_hash(val) if val.is_a?(Hash)
|
88
|
+
key = key.to_s if key.is_a?(Symbol)
|
89
|
+
normalized_hash[key] = val
|
90
|
+
end
|
49
91
|
end
|
50
92
|
end
|
51
93
|
end
|
52
|
-
|