Class: XCRes::StringsAnalyzer

Inherits:
Analyzer
  • Object
show all
Defined in:
lib/xcres/analyzer/strings_analyzer.rb

Overview

A StringsAnalyzer scans the project for resources, which should be included in the output file.

Instance Attribute Summary collapse

Attributes inherited from Analyzer

#exclude_file_patterns, #logger, #options, #sections, #target

Instance Method Summary collapse

Methods inherited from Analyzer

#filter_exclusions, #find_file_refs_by_extname, #is_file_ref_included_in_application_target?, #new_section, #project, #resources_files

Methods included from FileHelper

#basename_without_ext

Constructor Details

#initialize(project = nil, options = {}) ⇒ StringsAnalyzer

Initialize a new analyzer

Parameters:

  • project (Xcodeproj::Project) (defaults to: nil)

    see #project.

  • options (Hash) (defaults to: {})

    Possible options:

    • :default_language => see #default_language.



24
25
26
27
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 24

def initialize(project=nil, options={})
  super
  self.default_language = options[:default_language]
end

Instance Attribute Details

#default_languageString

Returns optional two-letter language code conforming ISO 639-1.

Returns:

  • (String)

    optional two-letter language code conforming ISO 639-1



13
14
15
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 13

def default_language
  @default_language
end

Instance Method Details

#absolute_info_plist_pathsSet<Pathname>

Absolute file paths to Info.plist files by build settings. See #info_plist_paths.

Returns:

  • (Set<Pathname>)

    the absolute paths to the .plist-files



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 136

def absolute_info_plist_paths
  info_plist_paths.map do |path|
    absolute_project_file_path(path)
  end.select do |path|
    if path.to_s.include?('$')
      warn "Couldn't resolve all placeholders in INFOPLIST_FILE %s.", path.to_s
      false
    else
      true
    end
  end
end

#absolute_project_file_path(file_path) ⇒ Pathname

Calculate the absolute path for a file path given relative to the project / its ‘$SRCROOT`.

We need either absolute paths or relative paths to our current location. Xcodeproj provides this for PBXFileReference, but this doesn’t work for file references in build settings.

Parameters:

  • file_path (String|Pathname)

    the path relative to the project.

Returns:

  • (Pathname)


214
215
216
217
218
219
220
221
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 214

def absolute_project_file_path(file_path)
  source_root = (project.path + '..').realpath
  if file_path.to_s.include?('$')
    Pathname(file_path.to_s.gsub(/\$[({]?SRCROOT[)}]?/, source_root.to_s))
  else
    source_root + file_path
  end
end

#analyzeObject



29
30
31
32
33
34
35
36
37
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 29

def analyze
  log 'Strings files in project: %s', strings_file_refs.map(&:path)
  log 'Native development languages: %s', native_dev_languages.to_a
  log 'Used languages for .strings files: %s', used_languages.to_a
  log 'Preferred languages: %s', languages.to_a
  log 'Strings files after language selection: %s', selected_strings_file_refs.map(&:path)

  @sections = [build_section]
end

#build_sectionSection

Build the section

Returns:



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 43

def build_section
  selected_file_refs = selected_strings_file_refs

  # Apply ignore list
  file_paths = filter_exclusions(selected_file_refs.map(&:path))
  filtered_file_refs = selected_file_refs.select { |file_ref| file_paths.include? file_ref.path }
  rel_file_paths = filtered_file_refs.map { |p| p.real_path.relative_path_from(Pathname.pwd) }

  log 'Non-ignored .strings files: %s', rel_file_paths.map(&:to_s)

  keys_by_file = {}
  for path in rel_file_paths
    keys_by_file[path] = keys_by_file(path)
  end
  items = keys_by_file.values.reduce({}, :merge)

  new_section('Strings', items)
end

#derive_used_languages(strings_file_refs) ⇒ Set<String>

Derive the used languages from given strings files

Parameters:

  • strings_file_refs (Array<PBXFileReference>)

Returns:

  • (Set<String>)


84
85
86
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 84

def derive_used_languages(strings_file_refs)
  strings_file_refs.map(&:name).to_set
end

#info_plist_pathsSet<Pathname>

Discover Info.plist files by build settings of the application target

Returns:

  • (Set<Pathname>)

    the relative paths to the .plist-files



124
125
126
127
128
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 124

def info_plist_paths
  @info_plist_paths ||= target.build_configurations.map do |config|
    config.build_settings['INFOPLIST_FILE']
  end.compact.map { |file| Pathname(file) }.flatten.to_set
end

#keys_by_file(path) ⇒ Hash{String => Hash}

Read a file and collect all its keys

Parameters:

  • path (Pathname)

    the path to the .strings file to read

Returns:

  • (Hash{String => Hash})


230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 230

def keys_by_file(path)
  begin
    # Load strings file contents
    strings = read_strings_file(path)

    # Reject generated identifiers used by Interface Builder
    strings.reject! { |key, _| /^[a-zA-Z0-9]{3}-[a-zA-Z0-9]{2,3}-[a-zA-Z0-9]{3}/.match(key) }

    keys = Hash[strings.map do |key, value|
      [key, { value: key, comment: value.gsub(/[\r\n]/, ' ') }]
    end]

    log 'Found %s keys in file %s', keys.count, path

    keys
  rescue ArgumentError => error
    raise ArgumentError, 'Error while reading %s: %s' % [path, error]
  end
end

#languagesSet<String>

Find preferred languages, which is:

- either only the default_language, if specified
- or the intersection of native development and used languages
- or all used languages

Returns:

  • (Set<String>)


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 103

def languages
  if default_language != nil
    # Use specified default language as primary language
    [default_language]
  else
    # Calculate the intersection of native development and used languages,
    # fallback to the latter only, if it is empty
    languages = native_dev_languages & used_languages
    if languages.empty?
      used_languages
    else
      languages
    end
  end
end

#native_dev_languagesSet<String>

Find the native development languages by trying to use the “Localization native development region” from Info.plist

Returns:

  • (Set<String>)


154
155
156
157
158
159
160
161
162
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 154

def native_dev_languages
  @native_dev_languages ||= absolute_info_plist_paths.map do |path|
    begin
      read_plist_key(path, :CFBundleDevelopmentRegion)
    rescue ArgumentError => e
      warn e
    end
  end.compact.to_set
end

#read_plist_key(path, key) ⇒ String

Extracts a given key from a plist file given as a path

Parameters:

  • path (Pathname)

    the path of the plist file

  • key (String)

    the key, whose value should been extracted

Returns:

  • (String)

Raises:

  • (ArgumentError)


174
175
176
177
178
179
180
181
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 174

def read_plist_key(path, key)
  raise ArgumentError, "File '#{path}' doesn't exist" unless path.exist?
  raise ArgumentError, 'Path is required, but nil' if path.nil?
  raise ArgumentError, 'Key is required, but nil' if key.nil?
  out = `/usr/libexec/PlistBuddy -c "Print :#{key}" "#{path}" 2>&1`.chomp
  raise ArgumentError, "Error reading plist: #{out}" unless $?.success?
  out
end

#read_strings_file(path) ⇒ Hash

Read a .strings file given as a path

Parameters:

  • path (Pathname)

    the path of the strings file

Returns:

  • (Hash)


190
191
192
193
194
195
196
197
198
199
200
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 190

def read_strings_file(path)
  raise ArgumentError, "File '#{path}' doesn't exist" unless path.exist?
  raise ArgumentError, "File '#{path}' is not a file" unless path.file?
  error = `plutil -lint -s "#{path}" 2>&1`
  raise ArgumentError, "File %s is malformed:\n#{error}" % path.to_s unless $?.success?
  json_or_error = `plutil -convert json "#{path}" -o -`.chomp
  raise ArgumentError, "File %s couldn't be converted to JSON.\n#{json_or_error}" % path.to_s unless $?.success?
  JSON.parse(json_or_error.force_encoding('UTF-8'))
rescue EncodingError => e
  raise StandardError, "Encoding error in #{path}: #{e}"
end

#selected_strings_file_refsArray<PBXFileReference>

Select strings files by language

Returns:

  • (Array<PBXFileReference>)


74
75
76
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 74

def selected_strings_file_refs
  @selected_strings_file_refs ||= strings_file_refs.select { |file_ref| languages.include? file_ref.name }
end

#strings_file_refsArray<PBXFileReference>

Discover all references to .strings files in project (e.g. Localizable.strings)

Returns:

  • (Array<PBXFileReference>)


66
67
68
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 66

def strings_file_refs
  @strings_file_refs ||= find_file_refs_by_extname '.strings'
end

#used_languagesSet<String>

All used languages in the project

Returns:

  • (Set<String>)


92
93
94
# File 'lib/xcres/analyzer/strings_analyzer.rb', line 92

def used_languages
  @used_languages ||= derive_used_languages(strings_file_refs)
end