Skip to content

Commit 8427fb8

Browse files
authored
Merge pull request #329 from wordpress-mobile/l10n/i2-i3
Introduce ios_merge_strings_files action
2 parents 4d991a0 + 5168121 commit 8427fb8

17 files changed

+482
-4
lines changed

.rubocop_todo.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ RSpec/FilePath:
143143
- 'spec/release_notes_helper_spec.rb'
144144
- 'spec/check_localization_progress_spec.rb'
145145
- 'spec/ios_generate_strings_file_from_code_spec.rb'
146+
- 'spec/ios_l10n_helper_spec.rb'
147+
- 'spec/ios_merge_strings_files_spec.rb'
146148

147149
# Offense count: 8
148150
# Cop supports --auto-correct.

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ _None_
1010

1111
### New Features
1212

13-
_None_
13+
* Introduce new `ios_merge_strings_files` action. [#329]
1414

1515
### Bug Fixes
1616

lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ def self.run(params)
1616
#####################################################
1717

1818
def self.description
19-
'Gathers the string to localise'
19+
'Gathers the strings to localise. Deprecated'
2020
end
2121

2222
def self.details
23-
'Gathers the string to localise. Deprecated in favor of the new `ios_generate_strings_file_from_code`'
23+
'Gathers the strings to localise. Deprecated in favor of the new `ios_generate_strings_file_from_code`'
2424
end
2525

2626
def self.category
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
module Fastlane
2+
module Actions
3+
class IosMergeStringsFilesAction < Action
4+
def self.run(params)
5+
UI.message "Merging strings files: #{params[:paths].inspect}"
6+
7+
duplicates = Fastlane::Helper::Ios::L10nHelper.merge_strings(paths: params[:paths], output_path: params[:destination])
8+
duplicates.each do |dup_key|
9+
UI.important "Duplicate key found while merging the `.strings` files: `#{dup_key}`"
10+
end
11+
duplicates
12+
end
13+
14+
#####################################################
15+
# @!group Documentation
16+
#####################################################
17+
18+
def self.description
19+
'Merge multiple `.strings` files into one'
20+
end
21+
22+
def self.details
23+
<<~DETAILS
24+
Merge multiple `.strings` files into one.
25+
26+
Especially useful to prepare a single `.strings` file merging strings from both `Localizable.strings` from
27+
the app code — typically previously extracted from `ios_generate_strings_file_from_code` —
28+
and string files like `InfoPlist.strings` — which values may not be generated from the codebase but
29+
manually maintained by developers.
30+
31+
The action only supports merging files which are in the OpenStep (`"key" = "value";`) text format (which is
32+
the most common format for `.strings` files, and the one generated by `genstrings`), but can handle the case
33+
of different files using different encodings (UTF8 vs UTF16) and is able to detect and report duplicates.
34+
It does not handle `.strings` files in XML or binary-plist formats (which are valid but more rare)
35+
DETAILS
36+
end
37+
38+
def self.available_options
39+
[
40+
FastlaneCore::ConfigItem.new(
41+
key: :paths,
42+
env_name: 'FL_IOS_MERGE_STRINGS_FILES_PATHS',
43+
description: 'The paths of all the `.strings` files to merge together',
44+
type: Array,
45+
optional: false
46+
),
47+
FastlaneCore::ConfigItem.new(
48+
key: :destination,
49+
env_name: 'FL_IOS_MERGE_STRINGS_FILES_DESTINATION',
50+
description: 'The path of the merged `.strings` file to generate. If nil, the merge will happen in-place in the first file in the `paths:` list',
51+
type: String,
52+
optional: true,
53+
default_value: nil
54+
),
55+
]
56+
end
57+
58+
def self.return_type
59+
:array_of_strings
60+
end
61+
62+
def self.return_value
63+
'The list of duplicate keys found while merging the various `.strings` files'
64+
end
65+
66+
def self.authors
67+
['automattic']
68+
end
69+
70+
def self.is_supported?(platform)
71+
[:ios, :mac].include? platform
72+
end
73+
end
74+
end
75+
end

lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_translators_strings.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,16 @@ def self.available_options
8585
]
8686
end
8787

88+
def self.category
89+
:deprecated
90+
end
91+
92+
def self.deprecated_notes
93+
'This action is deprecated. For a similar feature, you might want to check `ios_merge_strings_file` instead'
94+
end
95+
8896
def self.is_supported?(platform)
89-
true
97+
:ios
9098
end
9199
end
92100
end
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
require 'open3'
2+
require 'tempfile'
3+
require 'fileutils'
4+
require 'nokogiri'
5+
require 'json'
6+
7+
module Fastlane
8+
module Helper
9+
module Ios
10+
class L10nHelper
11+
# Returns the type of a `.strings` file (XML, binary or ASCII)
12+
#
13+
# @param [String] path The path to the `.strings` file to check
14+
# @return [Symbol] The file format used by the `.strings` file. Can be one of:
15+
# - `:text` for the ASCII-plist file format (containing typical `"key" = "value";` lines)
16+
# - `:xml` for XML plist file format (can be used if machine-generated, especially since there's no official way/tool to generate the ASCII-plist file format as output)
17+
# - `:binary` for binary plist file format (usually only true for `.strings` files converted by Xcode at compile time and included in the final `.app`/`.ipa`)
18+
# - `nil` if the file does not exist or is neither of those format (e.g. not a `.strings` file at all)
19+
#
20+
def self.strings_file_type(path:)
21+
# Start by checking it seems like a valid property-list file (and not e.g. an image or plain text file)
22+
_, status = Open3.capture2('/usr/bin/plutil', '-lint', path)
23+
return nil unless status.success?
24+
25+
# If it is a valid property-list file, determine the actual format used
26+
format_desc, status = Open3.capture2('/usr/bin/file', path)
27+
return nil unless status.success?
28+
29+
case format_desc
30+
when /Apple binary property list/ then return :binary
31+
when /XML/ then return :xml
32+
when /text/ then return :text
33+
end
34+
end
35+
36+
# Merge the content of multiple `.strings` files into a new `.strings` text file.
37+
#
38+
# @param [Array<String>] paths The paths of the `.strings` files to merge together
39+
# @param [String] into The path to the merged `.strings` file to generate as a result.
40+
# @return [Array<String>] List of duplicate keys found while validating the merge.
41+
#
42+
# @note For now, this method only supports merging `.strings` file in `:text` format
43+
# and basically concatenates the files (+ checking for duplicates in the process)
44+
# @note The method is able to handle input files which are using different encodings,
45+
# guessing the encoding of each input file using the BOM (and defaulting to UTF8).
46+
# The generated file will always be in utf-8, by convention.
47+
#
48+
# @raise [RuntimeError] If one of the paths provided is not in text format (but XML or binary instead), or if any of the files are missing.
49+
#
50+
def self.merge_strings(paths:, output_path: nil)
51+
duplicates = []
52+
Tempfile.create('wpmrt-l10n-merge-', encoding: 'utf-8') do |tmp_file|
53+
all_keys_found = []
54+
55+
tmp_file.write("/* Generated File. Do not edit. */\n\n")
56+
paths.each do |input_file|
57+
fmt = strings_file_type(path: input_file)
58+
raise "The file `#{input_file}` does not exist or is of unknown format." if fmt.nil?
59+
raise "The file `#{input_file}` is in #{fmt} format but we currently only support merging `.strings` files in text format." unless fmt == :text
60+
61+
string_keys = read_strings_file_as_hash(path: input_file).keys
62+
duplicates += (string_keys & all_keys_found) # Find duplicates using Array intersection, and add those to duplicates list
63+
all_keys_found += string_keys
64+
65+
tmp_file.write("/* MARK: - #{File.basename(input_file)} */\n\n")
66+
# Read line-by-line to reduce memory footprint during content copy; Be sure to guess file encoding using the Byte-Order-Mark.
67+
File.readlines(input_file, mode: 'rb:BOM|UTF-8').each { |line| tmp_file.write(line) }
68+
tmp_file.write("\n")
69+
end
70+
tmp_file.close # ensure we flush the content to disk
71+
FileUtils.cp(tmp_file.path, output_path)
72+
end
73+
duplicates
74+
end
75+
76+
# Return the list of translations in a `.strings` file.
77+
#
78+
# @param [String] path The path to the `.strings` file to read
79+
# @return [Hash<String,String>] A dictionary of key=>translation translations.
80+
# @raise [RuntimeError] If the file is not a valid strings file or there was an error in parsing its content.
81+
#
82+
def self.read_strings_file_as_hash(path:)
83+
output, status = Open3.capture2e('/usr/bin/plutil', '-convert', 'json', '-o', '-', path)
84+
raise output unless status.success?
85+
86+
JSON.parse(output)
87+
end
88+
89+
# Generate a `.strings` file from a dictionary of string translations.
90+
#
91+
# Especially useful to generate `.strings` files not from code, but from keys extracted from another source
92+
# (like a JSON file export from GlotPress, or subset of keys extracted from the main `Localizable.strings` to generate an `InfoPlist.strings`)
93+
#
94+
# @note The generated file will be in XML-plist format
95+
# since ASCII plist is deprecated as an output format by every Apple tool so there's no **safe** way to generate ASCII format.
96+
#
97+
# @param [Hash<String,String>] translations The dictionary of key=>translation translations to put in the generated `.strings` file
98+
# @param [String] output_path The path to the `.strings` file to generate
99+
#
100+
def self.generate_strings_file_from_hash(translations:, output_path:)
101+
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
102+
xml.doc.create_internal_subset(
103+
'plist',
104+
'-//Apple//DTD PLIST 1.0//EN',
105+
'http://www.apple.com/DTDs/PropertyList-1.0.dtd'
106+
)
107+
xml.comment('Warning: Auto-generated file, do not edit.')
108+
xml.plist(version: '1.0') do
109+
xml.dict do
110+
translations.each do |k, v|
111+
xml.key(k.to_s)
112+
xml.string(v.to_s)
113+
end
114+
end
115+
end
116+
end
117+
File.write(output_path, builder.to_xml)
118+
end
119+
end
120+
end
121+
end
122+
end

0 commit comments

Comments
 (0)