abroad 1.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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +142 -0
  3. data/abroad.gemspec +25 -0
  4. data/lib/abroad/extractors/extractor.rb +46 -0
  5. data/lib/abroad/extractors/json/json_extractor.rb +17 -0
  6. data/lib/abroad/extractors/json/key_value_extractor.rb +38 -0
  7. data/lib/abroad/extractors/json.rb +8 -0
  8. data/lib/abroad/extractors/xml/android_extractor.rb +113 -0
  9. data/lib/abroad/extractors/xml/xml_extractor.rb +28 -0
  10. data/lib/abroad/extractors/xml.rb +8 -0
  11. data/lib/abroad/extractors/yaml/dotted_key_extractor.rb +40 -0
  12. data/lib/abroad/extractors/yaml/jruby_compat.rb +170 -0
  13. data/lib/abroad/extractors/yaml/rails_extractor.rb +15 -0
  14. data/lib/abroad/extractors/yaml/yaml_extractor.rb +28 -0
  15. data/lib/abroad/extractors/yaml.rb +9 -0
  16. data/lib/abroad/extractors.rb +28 -0
  17. data/lib/abroad/serializers/json/json_serializer.rb +27 -0
  18. data/lib/abroad/serializers/json/key_value_serializer.rb +20 -0
  19. data/lib/abroad/serializers/json.rb +8 -0
  20. data/lib/abroad/serializers/serializer.rb +52 -0
  21. data/lib/abroad/serializers/trie.rb +76 -0
  22. data/lib/abroad/serializers/xml/android_serializer.rb +143 -0
  23. data/lib/abroad/serializers/xml/xml_serializer.rb +23 -0
  24. data/lib/abroad/serializers/xml.rb +8 -0
  25. data/lib/abroad/serializers/yaml/rails_serializer.rb +110 -0
  26. data/lib/abroad/serializers/yaml/yaml_serializer.rb +19 -0
  27. data/lib/abroad/serializers/yaml.rb +8 -0
  28. data/lib/abroad/serializers.rb +29 -0
  29. data/lib/abroad/version.rb +3 -0
  30. data/lib/abroad.rb +37 -0
  31. data/lib/ext/htmlentities/android_xml_decoder.rb +15 -0
  32. data/lib/ext/htmlentities/android_xml_encoder.rb +23 -0
  33. data/spec/abroad_spec.rb +35 -0
  34. data/spec/extractors/json/fixtures/arrays.json +9 -0
  35. data/spec/extractors/json/fixtures/basic.json +5 -0
  36. data/spec/extractors/json/fixtures/objects.json +11 -0
  37. data/spec/extractors/json/fixtures.yml +22 -0
  38. data/spec/extractors/json/json_extractor_spec.rb +6 -0
  39. data/spec/extractors/xml/fixtures/basic_arrays.xml +9 -0
  40. data/spec/extractors/xml/fixtures/basic_plurals.xml +8 -0
  41. data/spec/extractors/xml/fixtures/basic_strings.xml +8 -0
  42. data/spec/extractors/xml/fixtures/entities.xml +4 -0
  43. data/spec/extractors/xml/fixtures/markup.xml +5 -0
  44. data/spec/extractors/xml/fixtures/newlines.xml +6 -0
  45. data/spec/extractors/xml/fixtures/quotes.xml +9 -0
  46. data/spec/extractors/xml/fixtures.yml +54 -0
  47. data/spec/extractors/xml/xml_extractor_spec.rb +6 -0
  48. data/spec/extractors/yaml/fixtures/arrays.yml +9 -0
  49. data/spec/extractors/yaml/fixtures/arrays_and_hashes.yml +29 -0
  50. data/spec/extractors/yaml/fixtures/invalid_single_quote_escape.yml +2 -0
  51. data/spec/extractors/yaml/fixtures/invalid_single_quote_escape_array.yml +3 -0
  52. data/spec/extractors/yaml/fixtures/nesting.yml +19 -0
  53. data/spec/extractors/yaml/fixtures/short.yml +2 -0
  54. data/spec/extractors/yaml/fixtures.yml +75 -0
  55. data/spec/extractors/yaml/jruby_compat_spec.rb +54 -0
  56. data/spec/extractors/yaml/yaml_extractor_spec.rb +6 -0
  57. data/spec/serializers/json/key_value_serializer_spec.rb +26 -0
  58. data/spec/serializers/xml/android_serializer_spec.rb +165 -0
  59. data/spec/serializers/yaml/rails_serializer_spec.rb +171 -0
  60. data/spec/spec_helper.rb +43 -0
  61. metadata +186 -0
@@ -0,0 +1,28 @@
1
+ require 'yaml'
2
+
3
+ module Abroad
4
+ module Extractors
5
+ module Yaml
6
+
7
+ class YamlExtractor < Extractor
8
+ private
9
+
10
+ def parse
11
+ YAML.load(clean_yaml(stream.read))
12
+ rescue Psych::SyntaxError => e
13
+ raise Abroad::SyntaxError, "Syntax error in yaml: #{e.message}"
14
+ end
15
+
16
+ def clean_yaml(yaml_content)
17
+ if Abroad.jruby?
18
+ require 'abroad/extractors/yaml/jruby_compat'
19
+ JRubyCompat.clean(yaml_content)
20
+ else
21
+ yaml_content
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module Abroad
2
+ module Extractors
3
+ module Yaml
4
+ autoload :DottedKeyExtractor, 'abroad/extractors/yaml/dotted_key_extractor'
5
+ autoload :RailsExtractor, 'abroad/extractors/yaml/rails_extractor'
6
+ autoload :YamlExtractor, 'abroad/extractors/yaml/yaml_extractor'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ module Abroad
2
+ module Extractors
3
+ autoload :Extractor, 'abroad/extractors/extractor'
4
+ autoload :Json, 'abroad/extractors/json'
5
+ autoload :Xml, 'abroad/extractors/xml'
6
+ autoload :Yaml, 'abroad/extractors/yaml'
7
+
8
+ class << self
9
+ def register(id, klass)
10
+ registered[id.to_s] = klass
11
+ end
12
+
13
+ def get(id)
14
+ registered[id.to_s]
15
+ end
16
+
17
+ def available
18
+ registered.keys
19
+ end
20
+
21
+ private
22
+
23
+ def registered
24
+ @registered ||= {}
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ require 'json-write-stream'
2
+
3
+ module Abroad
4
+ module Serializers
5
+ module Json
6
+
7
+ class JsonSerializer < Serializer
8
+ attr_reader :writer
9
+
10
+ def initialize(stream, locale, encoding = Encoding::UTF_8)
11
+ super
12
+ @writer = JsonWriteStream.from_stream(stream, encoding)
13
+ end
14
+
15
+ def write_raw(text)
16
+ writer.stream.write(text)
17
+ end
18
+
19
+ def flush
20
+ writer.flush
21
+ stream.flush
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ module Abroad
2
+ module Serializers
3
+ module Json
4
+
5
+ class KeyValueSerializer < JsonSerializer
6
+ def initialize(stream, locale, encoding = Encoding::UTF_8)
7
+ super
8
+ writer.write_object
9
+ end
10
+
11
+ def write_key_value(key, value)
12
+ writer.write_key_value(
13
+ key.encode(encoding), value.encode(encoding)
14
+ )
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ module Abroad
2
+ module Serializers
3
+ module Json
4
+ autoload :JsonSerializer, 'abroad/serializers/json/json_serializer'
5
+ autoload :KeyValueSerializer, 'abroad/serializers/json/key_value_serializer'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,52 @@
1
+ module Abroad
2
+ module Serializers
3
+
4
+ class Serializer
5
+ attr_reader :stream, :locale, :encoding
6
+
7
+ class << self
8
+ def from_stream(stream, locale)
9
+ serializer = new(stream, locale)
10
+
11
+ if block_given?
12
+ yield(serializer).tap do
13
+ serializer.close
14
+ end
15
+ else
16
+ serializer
17
+ end
18
+ end
19
+
20
+ def open(file, locale, &block)
21
+ from_stream(File.open(file), locale, &block)
22
+ end
23
+ end
24
+
25
+ def initialize(stream, locale, encoding = Encoding::UTF_8)
26
+ @stream = stream
27
+ @locale = locale
28
+ @encoding = encoding
29
+ end
30
+
31
+ def write_key_value(key, value)
32
+ raise NotImplementedError,
33
+ 'expected to be implemented in derived classes'
34
+ end
35
+
36
+ def write_raw(text)
37
+ raise NotImplementedError,
38
+ 'expected to be implemented in derived classes'
39
+ end
40
+
41
+ def flush
42
+ stream.flush
43
+ end
44
+
45
+ def close
46
+ flush
47
+ stream.close
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ module Abroad
2
+ module Serializers
3
+
4
+ class Trie
5
+ attr_reader :root
6
+
7
+ def initialize(root = nil)
8
+ @root = root || TrieNode.new
9
+ end
10
+
11
+ def add(key_enum, value)
12
+ node = root
13
+
14
+ key_enum.each do |key|
15
+ if node.has_child?(key)
16
+ node = node.child_at(key)
17
+ else
18
+ node = node.add_child(key, TrieNode.new)
19
+ end
20
+ end
21
+
22
+ node.value = value
23
+ end
24
+
25
+ def find(key_enum)
26
+ node = root
27
+ key_enum.each do |key|
28
+ node = node.child_at(key)
29
+ return nil unless node
30
+ end
31
+ node.value
32
+ end
33
+ end
34
+
35
+ class TrieNode
36
+ NO_VALUE = :__novalue__
37
+
38
+ attr_reader :children
39
+ attr_accessor :value
40
+
41
+ def initialize(value = NO_VALUE)
42
+ @value = value
43
+ @children = {}
44
+ end
45
+
46
+ def each_child
47
+ if block_given?
48
+ children.each_pair { |key, child| yield key, child }
49
+ else
50
+ children.each
51
+ end
52
+ end
53
+
54
+ def has_children?
55
+ !children.empty?
56
+ end
57
+
58
+ def has_child?(key)
59
+ children.include?(key)
60
+ end
61
+
62
+ def child_at(key)
63
+ children[key]
64
+ end
65
+
66
+ def add_child(key, node)
67
+ @children[key] = node
68
+ end
69
+
70
+ def has_value?
71
+ value != NO_VALUE
72
+ end
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,143 @@
1
+ require 'htmlentities'
2
+ require 'ext/htmlentities/android_xml_encoder'
3
+
4
+ module Abroad
5
+ module Serializers
6
+ module Xml
7
+
8
+ class AndroidSerializer < XmlSerializer
9
+ PLURAL_FORMS = %w(
10
+ zero one two few many other
11
+ )
12
+
13
+ attr_reader :plurals, :arrays
14
+
15
+ def after_initialize
16
+ writer.open_tag('resources')
17
+ @plurals = Hash.new { |hash, key| hash[key] = {} }
18
+ @arrays = Hash.new { |hash, key| hash[key] = {} }
19
+ end
20
+
21
+ def write_raw(text)
22
+ stream.write(text)
23
+ end
24
+
25
+ def write_key_value(key, value)
26
+ key_parts = split_key(key)
27
+
28
+ case detect_string_type(key_parts)
29
+ when :plural
30
+ record_plural(key_parts, value)
31
+ when :array_element
32
+ record_array_element(key_parts, value)
33
+ else
34
+ write_string(key, value)
35
+ end
36
+ end
37
+
38
+ def flush
39
+ write_recorded_plurals
40
+ write_recorded_arrays
41
+ writer.flush
42
+ stream.flush
43
+ end
44
+
45
+ private
46
+
47
+ def split_key(key)
48
+ if idx = key.rindex('.')
49
+ [key[0..(idx - 1)], key[(idx + 1)..-1]]
50
+ else
51
+ # this case should never happen
52
+ [key, '']
53
+ end
54
+ end
55
+
56
+ def record_plural(key_parts, value)
57
+ plurals[key_parts.first][key_parts.last] = value
58
+ end
59
+
60
+ def record_array_element(key_parts, value)
61
+ arrays[key_parts.first][key_parts.last.to_i] = value
62
+ end
63
+
64
+ def write_plural(name, plural_forms)
65
+ writer.open_tag(:plurals, name: name)
66
+
67
+ plural_forms.each_pair do |quantity, value|
68
+ writer.open_single_line_tag(:item, quantity: quantity)
69
+ write_text(value)
70
+ writer.close_tag
71
+ end
72
+
73
+ writer.close_tag
74
+ end
75
+
76
+ def write_array(name, array_elements)
77
+ writer.open_tag(:'string-array', name: name)
78
+ count = array_elements.keys.max
79
+
80
+ (0..count).each do |i|
81
+ writer.open_single_line_tag(:item)
82
+ write_text(array_elements[i] || '')
83
+ writer.close_tag
84
+ end
85
+
86
+ writer.close_tag
87
+ end
88
+
89
+ def write_recorded_plurals
90
+ plurals.each_pair do |name, plural_forms|
91
+ write_plural(name, plural_forms)
92
+ end
93
+ end
94
+
95
+ def write_recorded_arrays
96
+ arrays.each_pair do |name, array_elements|
97
+ write_array(name, array_elements)
98
+ end
99
+ end
100
+
101
+ def write_string(key, value)
102
+ writer.open_single_line_tag(:string, name: key)
103
+ write_text(value)
104
+ writer.close_tag
105
+ end
106
+
107
+ def write_text(text)
108
+ escaped_text = escape(text)
109
+ writer.write_text(
110
+ escaped_text, escape: false
111
+ )
112
+ end
113
+
114
+ def detect_string_type(key_parts)
115
+ last_part = key_parts.last
116
+
117
+ if PLURAL_FORMS.include?(last_part)
118
+ :plural
119
+ elsif last_part =~ /\A[\d]+\z/
120
+ :array_element
121
+ else
122
+ :string
123
+ end
124
+ end
125
+
126
+ def escape(text)
127
+ text.gsub!("\n", "\\n") # escape literal newlines
128
+ text.gsub!("\r", "\\r") # escape literal carriage returns
129
+ text.gsub!("\t", "\\t") # escape literal tabs
130
+ text.gsub!(/([^\\]?)(')/) { "#{$1}\\'" } # escape single quotes
131
+ text.gsub!(/([^\\]?)(")/) { "#{$1}\\\"" } # escape double quotes
132
+
133
+ coder.encode(text)
134
+ end
135
+
136
+ def coder
137
+ @coder ||= HTMLEntities::AndroidXmlEncoder.new
138
+ end
139
+ end
140
+
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,23 @@
1
+ require 'xml-write-stream'
2
+
3
+ module Abroad
4
+ module Serializers
5
+ module Xml
6
+
7
+ class XmlSerializer < Serializer
8
+ attr_reader :writer
9
+
10
+ def initialize(stream, locale, encoding = Encoding::UTF_8)
11
+ super
12
+ @writer = XmlWriteStream.from_stream(stream)
13
+ writer.write_header(encoding: encoding.to_s)
14
+ after_initialize
15
+ end
16
+
17
+ def after_initialize
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,8 @@
1
+ module Abroad
2
+ module Serializers
3
+ module Xml
4
+ autoload :AndroidSerializer, 'abroad/serializers/xml/android_serializer'
5
+ autoload :XmlSerializer, 'abroad/serializers/xml/xml_serializer'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,110 @@
1
+ module Abroad
2
+ module Serializers
3
+ module Yaml
4
+
5
+ class RailsSerializer < YamlSerializer
6
+ attr_reader :trie
7
+
8
+ def initialize(stream, locale, encoding = Encoding::UTF_8)
9
+ super
10
+ @trie = Abroad::Serializers::Trie.new
11
+ end
12
+
13
+ def write_raw(text)
14
+ writer.stream.write(text)
15
+ end
16
+
17
+ def write_key_value(key, value)
18
+ key_parts = split_key(key)
19
+ encoded_value = value.encode(encoding)
20
+ trie.add(key_parts, encoded_value)
21
+ end
22
+
23
+ def flush
24
+ writer.write_map(locale)
25
+ write_node(trie.root, locale)
26
+ writer.flush
27
+ stream.flush
28
+ end
29
+
30
+ private
31
+
32
+ def split_key(key)
33
+ # Doesn't allow dots to come before spaces or at the end of the key.
34
+ # Uses regex negative lookahead, that's what the (?!) sections are.
35
+ # Examples:
36
+ # 'timezones.Solomon Is.' => ['timezones', 'Solomon Is.']
37
+ # 'timezones.Solomon Is.foo' => ['timezones', 'Solomon Is', 'foo']
38
+ # 'timezones.Solomon Is..foo' => ['timezones', 'Solomon Is.', 'foo']
39
+ key.split(/\.(?!\s)(?!\z)(?!\.)/)
40
+ end
41
+
42
+ # depth-first
43
+ def write_node(node, parent_key)
44
+ if node
45
+ if node.has_children?
46
+ if children_are_sequence(node)
47
+ write_sequence(node, parent_key)
48
+ else
49
+ write_map(node, parent_key)
50
+ end
51
+ elsif node.has_value?
52
+ write_value(node, parent_key)
53
+ end
54
+ else
55
+ write_value(node, parent_key)
56
+ end
57
+ end
58
+
59
+ def write_value(node, parent_key)
60
+ value = node ? node.value : ''
61
+ if writer.in_map?
62
+ writer.write_key_value(parent_key, value)
63
+ else
64
+ writer.write_element(value)
65
+ end
66
+ end
67
+
68
+ def write_map(node, parent_key)
69
+ if writer.in_map?
70
+ writer.write_map(parent_key)
71
+ else
72
+ writer.write_map
73
+ end
74
+
75
+ node.each_child do |key, child|
76
+ write_node(child, key)
77
+ end
78
+
79
+ writer.close_map
80
+ end
81
+
82
+ def write_sequence(node, parent_key)
83
+ if writer.in_map?
84
+ writer.write_sequence(parent_key)
85
+ else
86
+ writer.write_sequence
87
+ end
88
+
89
+ generate_sequence(node).each do |element|
90
+ write_node(element, nil)
91
+ end
92
+
93
+ writer.close_sequence
94
+ end
95
+
96
+ def children_are_sequence(node)
97
+ node.children.all? { |key, _| key =~ /\A[\d]+\z/ }
98
+ end
99
+
100
+ def generate_sequence(node)
101
+ keys = node.children.keys.map(&:to_i)
102
+ keys.each_with_object(Array.new(keys.max)) do |idx, arr|
103
+ arr[idx] = node.children[idx.to_s]
104
+ end
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,19 @@
1
+ require 'yaml'
2
+ require 'yaml-write-stream'
3
+
4
+ module Abroad
5
+ module Serializers
6
+ module Yaml
7
+
8
+ class YamlSerializer < Serializer
9
+ attr_reader :writer
10
+
11
+ def initialize(stream, locale, encoding = Encoding::UTF_8)
12
+ super
13
+ @writer = YamlWriteStream.from_stream(stream, encoding)
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ module Abroad
2
+ module Serializers
3
+ module Yaml
4
+ autoload :RailsSerializer, 'abroad/serializers/yaml/rails_serializer'
5
+ autoload :YamlSerializer, 'abroad/serializers/yaml/yaml_serializer'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ module Abroad
2
+ module Serializers
3
+ autoload :Json, 'abroad/serializers/json'
4
+ autoload :Serializer, 'abroad/serializers/serializer'
5
+ autoload :Trie, 'abroad/serializers/trie'
6
+ autoload :Xml, 'abroad/serializers/xml'
7
+ autoload :Yaml, 'abroad/serializers/yaml'
8
+
9
+ class << self
10
+ def register(id, klass)
11
+ registered[id] = klass
12
+ end
13
+
14
+ def get(id)
15
+ registered[id]
16
+ end
17
+
18
+ def available
19
+ registered.keys
20
+ end
21
+
22
+ private
23
+
24
+ def registered
25
+ @registered ||= {}
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Abroad
2
+ VERSION = '1.0.0'
3
+ end
data/lib/abroad.rb ADDED
@@ -0,0 +1,37 @@
1
+ module Abroad
2
+ class SyntaxError < StandardError; end
3
+
4
+ autoload :Extractors, 'abroad/extractors'
5
+ autoload :Serializers, 'abroad/serializers'
6
+
7
+ Extractors.register('yaml/rails', Extractors::Yaml::RailsExtractor)
8
+ Extractors.register('yaml/dotted-key', Extractors::Yaml::DottedKeyExtractor)
9
+ Extractors.register('json/key-value', Extractors::Json::KeyValueExtractor)
10
+ Extractors.register('xml/android', Extractors::Xml::AndroidExtractor)
11
+
12
+ Serializers.register('yaml/rails', Serializers::Yaml::RailsSerializer)
13
+ Serializers.register('json/key-value', Serializers::Json::KeyValueSerializer)
14
+ Serializers.register('xml/android', Serializers::Xml::AndroidSerializer)
15
+
16
+ class << self
17
+ def extractors
18
+ Extractors.available
19
+ end
20
+
21
+ def extractor(id)
22
+ Extractors.get(id)
23
+ end
24
+
25
+ def serializers
26
+ Serializers.available
27
+ end
28
+
29
+ def serializer(id)
30
+ Serializers.get(id)
31
+ end
32
+
33
+ def jruby?
34
+ RUBY_ENGINE == 'jruby'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ require 'htmlentities'
2
+
3
+ class HTMLEntities
4
+ MAPPINGS['android_xml'] = MAPPINGS['xhtml1'].dup.tap do |mappings|
5
+ mappings.delete('apos')
6
+ end
7
+
8
+ FLAVORS << 'android_xml'
9
+
10
+ class AndroidXmlDecoder < Decoder
11
+ def initialize
12
+ super('android_xml')
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ require 'htmlentities'
2
+
3
+ class HTMLEntities
4
+ MAPPINGS['android_xml'] = MAPPINGS['xhtml1'].dup.tap do |mappings|
5
+ mappings.delete('apos')
6
+ end
7
+
8
+ FLAVORS << 'android_xml'
9
+
10
+ class AndroidXmlEncoder < Encoder
11
+ def initialize(instructions = [])
12
+ super('android_xml', instructions)
13
+ end
14
+
15
+ private
16
+
17
+ # had to adjust this so that single and double quotes don't get turned into
18
+ # entities (they're escaped by hand with backslashes)
19
+ def basic_entity_regexp
20
+ @basic_entity_regexp ||= /[&]/
21
+ end
22
+ end
23
+ end