-
Notifications
You must be signed in to change notification settings - Fork 83
Add support for Xcode String Catalog #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| require "json" | ||
| require_relative "csv2base" | ||
|
|
||
| module Babelish | ||
| class CSV2XCStrings < Csv2Base | ||
| attr_accessor :languages | ||
|
|
||
| def language_filepaths(language) | ||
| require 'pathname' | ||
| filepaths = [] | ||
| if language.regions.empty? | ||
| filepaths << Pathname.new(@output_dir) + "#{output_basename}.#{extension}" | ||
| else | ||
| language.regions.each do |region| | ||
| filepaths << Pathname.new(@output_dir) + "#{output_basename}.#{extension}" | ||
| end | ||
| end | ||
| filepaths | ||
| end | ||
|
|
||
| def extension | ||
| "xcstrings" | ||
| end | ||
|
|
||
| def output_basename | ||
| @output_basename || 'Localizable' | ||
| end | ||
|
|
||
| def write_content | ||
| info = "List of created files:\n" | ||
| count = 0 | ||
|
|
||
| file_path = @ignore_lang_path ? default_filepath : Pathname.new(@output_dir) + "#{output_basename}.#{extension}" | ||
| file = create_file_from_path(file_path) | ||
|
|
||
| xcstrings_data = { | ||
| "sourceLanguage" => determine_source_language, | ||
| "strings" => {}, | ||
| "version" => "1.0" | ||
| } | ||
|
|
||
| keys.each do |key| | ||
| next if key.nil? || key.empty? | ||
|
|
||
| string_entry = { | ||
| "extractionState" => "manual" | ||
| } | ||
|
|
||
| if @comments[key] && !@comments[key].empty? | ||
| string_entry["comment"] = @comments[key] | ||
| end | ||
|
|
||
| localizations = {} | ||
| @languages.each do |language| | ||
| next if language.nil? || language.content.nil? | ||
|
|
||
| value = language.content[key] | ||
| next if value.nil? || value.empty? | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| if language.regions && !language.regions.empty? | ||
| language.regions.each do |region| | ||
| lang_code = "#{language.code}-#{region}" | ||
| next if lang_code.nil? | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| localizations[lang_code] = { | ||
| "stringUnit" => { | ||
| "state" => "translated", | ||
| "value" => value | ||
| } | ||
| } | ||
| end | ||
| else | ||
| lang_code = language.code | ||
| next if lang_code.nil? | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| localizations[lang_code] = { | ||
| "stringUnit" => { | ||
| "state" => "translated", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| "value" => value | ||
| } | ||
| } | ||
| end | ||
| end | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| string_entry["localizations"] = localizations unless localizations.empty? | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [81/80] |
||
| xcstrings_data["strings"][key] = string_entry unless localizations.empty? | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [81/80] |
||
| end | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| file.write(JSON.pretty_generate(xcstrings_data)) | ||
| info += "- #{File.absolute_path(file)}\n" | ||
| count += 1 | ||
| file.close | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| info = "Created #{count} files.\n" + info | ||
| return info | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/RedundantReturn: Redundant return detected. |
||
| end | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| private | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| def determine_source_language | ||
| source_lang = "en" | ||
|
|
||
| @languages.each do |language| | ||
| next if language.nil? | ||
|
|
||
| if language.regions.any? { |code| code == "en" || code.start_with?("en-") } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [83/80] |
||
| source_lang = "en" | ||
| break | ||
| elsif language.code == "en" || language.code&.start_with?("en-") | ||
| source_lang = "en" | ||
| break | ||
| end | ||
| end | ||
|
|
||
| if source_lang == "en" && @languages.none? { |lang| lang&.code == "en" || lang&.regions&.include?("en") } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [111/80] |
||
| first_lang = @languages.detect { |lang| !lang.nil? && !lang.regions.empty? } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [84/80] |
||
| source_lang = first_lang&.regions&.first || first_lang&.code || "en" | ||
| end | ||
|
|
||
| source_lang | ||
| end | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/TrailingWhitespace: Trailing whitespace detected. |
||
| def hash_to_output(*) | ||
| "" | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| require 'json' | ||
| require 'set' | ||
| require_relative 'base2csv' | ||
|
|
||
| module Babelish | ||
| class XCStrings2CSV < Base2Csv | ||
| attr_accessor :csv_filename, :headers, :filenames, :default_lang | ||
|
|
||
| def initialize(args = {:filenames => []}) | ||
| super(args) | ||
| end | ||
|
|
||
| # Load all strings of a given file | ||
| def load_strings(xcstrings_filename) | ||
| strings = {} | ||
| comments = {} | ||
|
|
||
| begin | ||
| file_content = File.read(xcstrings_filename) | ||
| xcstrings_data = JSON.parse(file_content) | ||
|
|
||
| lang_code = extract_language_from_filename(xcstrings_filename, xcstrings_data) | ||
|
|
||
| xcstrings_data["strings"]&.each do |key, entry| | ||
| comments[key] = entry["comment"] if entry["comment"] | ||
|
|
||
| localizations = entry["localizations"] || {} | ||
|
|
||
| translation = find_translation_for_language(localizations, lang_code, xcstrings_data["sourceLanguage"]) | ||
|
|
||
| if translation | ||
| strings[key] = translation | ||
| end | ||
| end | ||
|
|
||
| rescue JSON::ParserError => e | ||
| puts "Error parsing xcstrings file #{xcstrings_filename}: #{e.message}" | ||
| return [{}, {}] | ||
| rescue => e | ||
| puts "Error reading xcstrings file #{xcstrings_filename}: #{e.message}" | ||
| return [{}, {}] | ||
| end | ||
|
|
||
| [strings, comments] | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def extract_language_from_filename(filename, xcstrings_data) | ||
| source_language = xcstrings_data["sourceLanguage"] || "en" | ||
|
|
||
| basename = File.basename(filename, ".xcstrings") | ||
| if basename.include?("-") | ||
| potential_lang = basename.split("-").last | ||
| return potential_lang if potential_lang.length == 2 || potential_lang.include?("_") | ||
| end | ||
|
|
||
| source_language | ||
| end | ||
|
|
||
| def find_translation_for_language(localizations, target_lang, source_lang) | ||
| if localizations[target_lang]&.dig("stringUnit", "value") | ||
| return localizations[target_lang]["stringUnit"]["value"] | ||
| end | ||
|
|
||
| base_lang = target_lang.split("-").first | ||
| if localizations[base_lang]&.dig("stringUnit", "value") | ||
| return localizations[base_lang]["stringUnit"]["value"] | ||
| end | ||
|
|
||
| localizations.each do |lang_code, localization| | ||
| if lang_code.start_with?(base_lang + "-") && localization.dig("stringUnit", "value") | ||
| return localization["stringUnit"]["value"] | ||
| end | ||
| end | ||
|
|
||
| if localizations[source_lang]&.dig("stringUnit", "value") | ||
| return localizations[source_lang]["stringUnit"]["value"] | ||
| end | ||
|
|
||
| localizations.each do |lang_code, localization| | ||
| value = localization.dig("stringUnit", "value") | ||
| return value if value | ||
| end | ||
|
|
||
| nil | ||
| end | ||
|
|
||
| public | ||
| def convert(write_to_file = true) | ||
| all_strings = {} | ||
| all_keys = Set.new | ||
| all_comments = {} | ||
| all_languages = Set.new | ||
|
|
||
| @filenames.each do |fname| | ||
| begin | ||
| file_content = File.read(fname) | ||
| xcstrings_data = JSON.parse(file_content) | ||
|
|
||
| xcstrings_data["strings"]&.each do |key, entry| | ||
| all_keys.add(key) | ||
| all_comments[key] = entry["comment"] if entry["comment"] | ||
|
|
||
| entry["localizations"]&.each do |lang_code, localization| | ||
| all_languages.add(lang_code) | ||
| end | ||
| end | ||
| rescue => e | ||
| puts "Error processing #{fname}: #{e.message}" | ||
| next | ||
| end | ||
| end | ||
|
|
||
| all_languages.each do |lang_code| | ||
| lang_strings = {} | ||
|
|
||
| @filenames.each do |fname| | ||
| begin | ||
| file_content = File.read(fname) | ||
| xcstrings_data = JSON.parse(file_content) | ||
|
|
||
| xcstrings_data["strings"]&.each do |key, entry| | ||
| localizations = entry["localizations"] || {} | ||
| translation = find_translation_for_language(localizations, lang_code, xcstrings_data["sourceLanguage"]) | ||
| lang_strings[key] = translation if translation | ||
| end | ||
| rescue => e | ||
| next | ||
| end | ||
| end | ||
|
|
||
| all_strings[lang_code] = lang_strings | ||
| end | ||
|
|
||
| if write_to_file | ||
| puts "Creating #{@csv_filename}" | ||
| create_csv_file_for_xcstrings(all_keys.to_a, all_strings, all_comments, all_languages.to_a) | ||
| else | ||
| return all_keys.to_a, all_strings | ||
| end | ||
| end | ||
|
|
||
| def create_csv_file_for_xcstrings(keys, strings, comments, languages) | ||
| require 'csv' | ||
|
|
||
| raise "csv_filename must not be nil" unless @csv_filename | ||
|
|
||
| headers = ["Variables"] | ||
| languages.sort.each { |lang| headers << lang } | ||
| headers << "Comments" if !comments.nil? && !comments.empty? | ||
|
|
||
| CSV.open(@csv_filename, "wb") do |csv| | ||
| csv << headers | ||
|
|
||
| keys.each do |key| | ||
| line = [key] | ||
|
|
||
| languages.sort.each do |lang| | ||
| value = strings[lang] ? strings[lang][key] : "" | ||
| line << (value || "") | ||
| end | ||
|
|
||
| line << comments[key] if comments && comments[key] | ||
| csv << line | ||
| end | ||
|
|
||
| puts "Done" | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style/FrozenStringLiteralComment: Missing magic comment # frozen_string_literal: true.