abroad 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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