localio 0.1.6 → 0.2.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 (51) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +28 -0
  3. data/.gitignore +3 -1
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile.lock +138 -0
  7. data/README.md +34 -35
  8. data/bin/localize +10 -7
  9. data/docs/plans/2026-02-23-modernization-design.md +91 -0
  10. data/docs/plans/2026-02-23-modernization.md +1699 -0
  11. data/lib/localio/processor.rb +2 -1
  12. data/lib/localio/processors/csv_processor.rb +12 -3
  13. data/lib/localio/processors/google_drive_processor.rb +36 -48
  14. data/lib/localio/processors/xls_processor.rb +12 -3
  15. data/lib/localio/processors/xlsx_processor.rb +12 -3
  16. data/lib/localio/template_handler.rb +3 -1
  17. data/lib/localio/templates/android_localizable.erb +14 -2
  18. data/lib/localio/templates/ios_constant_localizable.erb +16 -2
  19. data/lib/localio/templates/ios_localizable.erb +20 -5
  20. data/lib/localio/templates/java_properties_localizable.erb +16 -2
  21. data/lib/localio/templates/json_localizable.erb +6 -5
  22. data/lib/localio/templates/rails_localizable.erb +15 -3
  23. data/lib/localio/templates/resx_localizable.erb +14 -2
  24. data/lib/localio/templates/swift_constant_localizable.erb +15 -2
  25. data/lib/localio/version.rb +1 -1
  26. data/lib/localio/writers/ios_writer.rb +3 -3
  27. data/lib/localio/writers/swift_writer.rb +3 -3
  28. data/localio.gemspec +19 -25
  29. data/spec/fixtures/sample.csv +11 -0
  30. data/spec/localio/filter_spec.rb +40 -0
  31. data/spec/localio/formatter_spec.rb +32 -0
  32. data/spec/localio/processors/csv_processor_spec.rb +89 -0
  33. data/spec/localio/processors/google_drive_processor_spec.rb +107 -0
  34. data/spec/localio/processors/xls_processor_spec.rb +65 -0
  35. data/spec/localio/processors/xlsx_processor_spec.rb +59 -0
  36. data/spec/localio/segment_spec.rb +27 -0
  37. data/spec/localio/segments_list_holder_spec.rb +22 -0
  38. data/spec/localio/string_helper_spec.rb +49 -0
  39. data/spec/localio/template_handler_spec.rb +67 -0
  40. data/spec/localio/term_spec.rb +24 -0
  41. data/spec/localio/writers/android_writer_spec.rb +71 -0
  42. data/spec/localio/writers/ios_writer_spec.rb +63 -0
  43. data/spec/localio/writers/java_properties_writer_spec.rb +35 -0
  44. data/spec/localio/writers/json_writer_spec.rb +57 -0
  45. data/spec/localio/writers/rails_writer_spec.rb +47 -0
  46. data/spec/localio/writers/resx_writer_spec.rb +44 -0
  47. data/spec/localio/writers/swift_writer_spec.rb +42 -0
  48. data/spec/localio_spec.rb +62 -0
  49. data/spec/spec_helper.rb +24 -0
  50. data/spec/support/shared_terms.rb +35 -0
  51. metadata +60 -49
@@ -5,6 +5,7 @@ require 'localio/processors/csv_processor'
5
5
 
6
6
  module Processor
7
7
  def self.load_localizables(platform_options, service, options)
8
+ puts "Service: #{service}"
8
9
  case service
9
10
  when :google_drive
10
11
  GoogleDriveProcessor.load_localizables platform_options, options
@@ -18,4 +19,4 @@ module Processor
18
19
  raise ArgumentError, 'Unsupported service! Try with :google_drive, :csv, :xlsx or :xls in the source argument'
19
20
  end
20
21
  end
21
- end
22
+ end
@@ -36,8 +36,17 @@ class CsvProcessor
36
36
  for column in 1..csv_file[first_valid_row_index].count-1
37
37
  col_all = csv_file[first_valid_row_index][column].to_s
38
38
  col_all.each_line(' ') do |col_text|
39
- default_language = col_text.downcase.gsub('*', '') if col_text.include? '*'
40
- languages.store col_text.downcase.gsub('*', ''), column unless col_text.to_s == ''
39
+ default_language = col_text.gsub('*', '') if col_text.include? '*'
40
+ lang = col_text.gsub('*', '')
41
+
42
+ unless platform_options[:avoid_lang_downcase]
43
+ default_language = default_language.downcase
44
+ lang = lang.downcase
45
+ end
46
+
47
+ unless col_text.to_s == ''
48
+ languages.store lang, column
49
+ end
41
50
  end
42
51
  end
43
52
 
@@ -78,4 +87,4 @@ class CsvProcessor
78
87
 
79
88
  end
80
89
 
81
- end
90
+ end
@@ -1,6 +1,5 @@
1
1
  require 'google_drive'
2
2
  require 'localio/term'
3
- require 'localio/config_store'
4
3
 
5
4
  class GoogleDriveProcessor
6
5
 
@@ -19,9 +18,11 @@ class GoogleDriveProcessor
19
18
  client_id = options[:client_id]
20
19
  client_secret = options[:client_secret]
21
20
 
22
- # We need client_id / client_secret
23
- raise ArgumentError, ':client_id required for Google Drive. Check how to get it here: https://developers.google.com/drive/web/auth/web-server' if client_id.nil?
24
- raise ArgumentError, ':client_secret required for Google Drive. Check how to get it here: https://developers.google.com/drive/web/auth/web-server' if client_secret.nil?
21
+ # We need client_id / client_secret (unless a service-account key is supplied)
22
+ unless options[:client_token].is_a?(String) && File.file?(options[:client_token].to_s)
23
+ raise ArgumentError, ':client_id required for Google Drive. Check how to get it here: https://developers.google.com/drive/web/auth/web-server' if client_id.nil?
24
+ raise ArgumentError, ':client_secret required for Google Drive. Check how to get it here: https://developers.google.com/drive/web/auth/web-server' if client_secret.nil?
25
+ end
25
26
 
26
27
  override_default = nil
27
28
  override_default = platform_options[:override_default] unless platform_options.nil? or platform_options[:override_default].nil?
@@ -29,50 +30,23 @@ class GoogleDriveProcessor
29
30
  # Log in and get spreadsheet
30
31
  puts 'Logging in to Google Drive...'
31
32
  begin
32
- client = Google::APIClient.new application_name: 'Localio', application_version: Localio::VERSION
33
- auth = client.authorization
34
- auth.client_id = client_id
35
- auth.client_secret = client_secret
36
- auth.scope =
37
- "https://docs.google.com/feeds/" +
38
- "https://www.googleapis.com/auth/drive " +
39
- "https://spreadsheets.google.com/feeds/"
40
- auth.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
41
-
42
- config = ConfigStore.new
43
-
44
- access_token = nil
45
-
46
- if options.has_key?(:client_token)
47
- puts 'Refreshing auth token...'
48
- auth.refresh_token = options[:client_token]
49
- auth.refresh!
50
- access_token = auth.access_token
51
- elsif config.has? :refresh_token
52
- puts 'Refreshing auth token...'
53
- auth.refresh_token = config.get :refresh_token
54
- auth.refresh!
55
- access_token = auth.access_token
33
+ session = nil
34
+
35
+ # Service-account key file (JSON) path supplied via :client_token
36
+ if options[:client_token].is_a?(String) && File.file?(options[:client_token].to_s)
37
+ session = GoogleDrive::Session.from_service_account_key(options[:client_token])
56
38
  else
57
- puts "1. Open this page in your browser:\n#{auth.authorization_uri}\n\n"
58
- puts "2. Enter the authorization code shown in the page: "
59
- auth.code = $stdin.gets.chomp
60
- auth.fetch_access_token!
61
- access_token = auth.access_token
39
+ # OAuth2 config-file flow (from_config saves/loads the refresh token)
40
+ config_path = File.join(Dir.home, '.localio_gdrive_config.json')
41
+ session = GoogleDrive::Session.from_config(
42
+ config_path,
43
+ client_id: client_id,
44
+ client_secret: client_secret
45
+ )
62
46
  end
63
-
64
- if !options.has_key?(:client_token)
65
- puts 'Store auth data...'
66
- config.store :refresh_token, auth.refresh_token
67
- config.store :access_token, auth.access_token
68
- config.persist
69
- end
70
-
71
- # Creates a session
72
- session = GoogleDrive.login_with_oauth(access_token)
73
47
  rescue => e
74
48
  puts "Error: #{e.inspect}"
75
- raise 'Couldn\'t access Google Drive. Check your values for :client_id and :client_secret, and delete :access_token if present (you might need to refresh its value so please remove it)'
49
+ raise 'Couldn\'t access Google Drive. Check your values for :client_id and :client_secret.'
76
50
  end
77
51
  puts 'Logged in!'
78
52
 
@@ -90,9 +64,14 @@ class GoogleDriveProcessor
90
64
  abort "More than one match found (#{matching_spreadsheets.join ', '}). You have to be more specific!"
91
65
  end
92
66
 
67
+ sheet = options[:sheet]
68
+ worksheet = if sheet.is_a? Integer
69
+ matching_spreadsheets[0].worksheets[sheet]
70
+ elsif sheet.is_a? String
71
+ matching_spreadsheets[0].worksheets.detect { |s| s.title == sheet }
72
+ end
73
+
93
74
 
94
- # TODO we could pass a :page_index in the options hash and get that worksheet instead, defaulting to zero?
95
- worksheet = matching_spreadsheets[0].worksheets[0]
96
75
  raise 'Unable to retrieve the first worksheet from the spreadsheet. Are there any pages?' if worksheet.nil?
97
76
 
98
77
  # At this point we have the worksheet, so we want to store all the key / values
@@ -114,8 +93,17 @@ class GoogleDriveProcessor
114
93
  for column in 2..worksheet.max_cols
115
94
  col_all = worksheet[first_valid_row_index, column]
116
95
  col_all.each_line(' ') do |col_text|
117
- default_language = col_text.downcase.gsub('*', '') if col_text.include? '*'
118
- languages.store col_text.downcase.gsub('*', ''), column unless col_text.to_s == ''
96
+ default_language = col_text.gsub('*', '') if col_text.include? '*'
97
+ lang = col_text.gsub('*', '')
98
+
99
+ unless platform_options[:avoid_lang_downcase]
100
+ default_language = default_language.downcase
101
+ lang = lang.downcase
102
+ end
103
+
104
+ unless col_text.to_s == ''
105
+ languages.store lang, column
106
+ end
119
107
  end
120
108
  end
121
109
 
@@ -40,8 +40,17 @@ class XlsProcessor
40
40
  for column in 1..worksheet.column_count
41
41
  col_all = worksheet[first_valid_row_index, column].to_s
42
42
  col_all.each_line(' ') do |col_text|
43
- default_language = col_text.downcase.gsub('*','') if col_text.include? '*'
44
- languages.store col_text.downcase.gsub('*',''), column unless col_text.to_s == ''
43
+ default_language = col_text.gsub('*', '') if col_text.include? '*'
44
+ lang = col_text.gsub('*', '')
45
+
46
+ unless platform_options[:avoid_lang_downcase]
47
+ default_language = default_language.downcase
48
+ lang = lang.downcase
49
+ end
50
+
51
+ unless col_text.to_s == ''
52
+ languages.store lang, column
53
+ end
45
54
  end
46
55
  end
47
56
 
@@ -82,4 +91,4 @@ class XlsProcessor
82
91
 
83
92
  end
84
93
 
85
- end
94
+ end
@@ -43,8 +43,17 @@ class XlsxProcessor
43
43
  for column in 1..worksheet.rows[first_valid_row_index].count-1
44
44
  col_all = worksheet.rows[first_valid_row_index][column].to_s
45
45
  col_all.each_line(' ') do |col_text|
46
- default_language = col_text.downcase.gsub('*','') if col_text.include? '*'
47
- languages.store col_text.downcase.gsub('*',''), column unless col_text.to_s == ''
46
+ default_language = col_text.gsub('*', '') if col_text.include? '*'
47
+ lang = col_text.gsub('*', '')
48
+
49
+ unless platform_options[:avoid_lang_downcase]
50
+ default_language = default_language.downcase
51
+ lang = lang.downcase
52
+ end
53
+
54
+ unless col_text.to_s == ''
55
+ languages.store lang, column
56
+ end
48
57
  end
49
58
  end
50
59
 
@@ -85,4 +94,4 @@ class XlsxProcessor
85
94
 
86
95
  end
87
96
 
88
- end
97
+ end
@@ -6,11 +6,13 @@ class TemplateHandler
6
6
  full_template_path = File.join(File.dirname(File.expand_path(__FILE__)), "templates/#{template_name}")
7
7
  input_file = File.open(full_template_path, 'rb')
8
8
  template = input_file.read
9
+
9
10
  input_file.close
10
11
  renderer = ERB.new(template)
11
12
  output = renderer.result(segments.get_binding)
12
13
  output_file = File.new(generated_file_name, 'w')
13
- output_file.write(output)
14
+ output_replace = output.gsub(",}", "}")
15
+ output_file.write(output_replace)
14
16
  output_file.close
15
17
 
16
18
  destination_path = File.join(target_directory, generated_file_name)
@@ -1,11 +1,23 @@
1
1
  <!-- Localizable created with localio. DO NOT MODIFY. -->
2
2
  <resources>
3
3
  <%
4
+ node_keys =[]
4
5
  @segments.each do |term|
5
6
  if term.is_comment? %>
6
7
  <!-- <%= term.translation %> -->
7
- <% else %> <string name="<%= term.key %>"><%= term.translation %></string>
8
- <% end
8
+ <% else
9
+ if term.key == '[init-node]' or term.key == '[end-node]'
10
+ node_keys << term.translation if term.key == '[init-node]'
11
+ node_keys.pop if term.key == '[end-node]'
12
+ else
13
+ if node_keys.length() > 0
14
+ key_join = node_keys.join("_")+"_"+term.key
15
+ else
16
+ key_join = term.key
17
+ end
18
+ %> <string name="<%= key_join %>"><%= term.translation %></string>
19
+ <% end
20
+ end
9
21
  end
10
22
  %>
11
23
  </resources>
@@ -6,6 +6,20 @@ GENERATED - DO NOT MODIFY - use localio instead.
6
6
  Created by localio.
7
7
  */
8
8
 
9
- <% @segments.each do |term| %>
10
- #define <%= term.key %> NSLocalizedString(@"<%= term.translation %>",nil)<%
9
+ <%
10
+ node_keys = []
11
+ @segments.each do |term|
12
+ if term.key == '[init-node]' or term.key == '[end-node]'
13
+ node_keys << term.translation if term.key == '[init-node]'
14
+ node_keys.pop if term.key == '[end-node]'
15
+ else
16
+ if node_keys.length() >0
17
+ key_join = node_keys.join("_").capitalize+"_"+term.key.downcase
18
+ else
19
+ key_join = term.key
20
+ end
21
+ %>
22
+ #define kLocale<%= key_join %> NSLocalizedString(@"_<%= key_join %>",nil)
23
+ <%
24
+ end
11
25
  end %>
@@ -6,12 +6,27 @@ GENERATED - DO NOT MODIFY - use the localio gem instead.
6
6
  Created by localio.
7
7
  */
8
8
 
9
- <% @segments.each do |term| %>
10
- <%
11
- if term.is_comment?
9
+ <%
10
+ node_keys = []
11
+ @segments.each do |term|
12
+ if term.is_comment?
13
+ %>
14
+ // <%= term.translation %>
15
+ <%
16
+ else
17
+ if term.key == '[init-node]' or term.key == '[end-node]'
18
+ node_keys << term.translation if term.key == '[init-node]'
19
+ node_keys.pop if term.key == '[end-node]'
20
+ else
21
+ if node_keys.length() > 0
22
+ key_join = node_keys.join("_").capitalize+"_"+term.key.downcase
23
+ else
24
+ key_join = term.key
25
+ end
12
26
  %>
13
- // <%= term.translation %>
14
- <% else %>"<%= term.key %>" = "<%= term.translation %>";<%
27
+ "_<%= key_join %>" = "<%= term.translation %>";
28
+ <%
29
+ end
15
30
  end
16
31
  end
17
32
  %>
@@ -1,10 +1,24 @@
1
1
  # Localizable created with localio. DO NOT MODIFY.
2
2
  #
3
3
  # Language: <%= @language %>
4
- <% @segments.each do |term|
4
+ <%
5
+ node_keys = []
6
+ @segments.each do |term|
5
7
  if term.is_comment? %>
6
8
  # <%= term.translation %>
7
- <% else %><%= term.key %>=<%= term.translation %>
9
+ <%
10
+ else
11
+ if term.key == '[init-node]' or term.key == '[end-node]'
12
+ node_keys << term.translation if term.key == '[init-node]'
13
+ node_keys.pop if term.key == '[end-node]'
14
+ else
15
+ if node_keys.length() > 0
16
+ key_join = node_keys.join("_")+"_"+term.key
17
+ else
18
+ key_join = term.key
19
+ end
20
+ %><%= key_join %>=<%= term.translation %>
8
21
  <% end
9
22
  end
23
+ end
10
24
  %>
@@ -4,12 +4,13 @@
4
4
  "language": "<%= @language %>"
5
5
  },
6
6
  "translations": {
7
- <% @segments.each do |term|
7
+ <% @segments.each.with_index do |term, index|
8
8
  term_value = term.translation
9
9
  term_key = term.key
10
-
11
- term_key = '___comment___' if term.is_comment?
12
- %> "<%= term_key %>": "<%= term_value %>"<% unless term == @segments.last %>,<% end %>
13
- <% end %>
10
+ count_to_string = index.to_s
11
+ term_key = '___comment_'+count_to_string+'___' if term.is_comment?
12
+ if term.key == '[init-node]'%>
13
+ "<%= term_value %>": {<% elsif term.key == '[end-node]'%>}<% unless term == @segments.last %>,<% end %><% else %>
14
+ "<%= term_key %>": "<%= term_value %>"<% unless term == @segments.last %>,<% end %><% end end %>
14
15
  }
15
16
  }
@@ -4,10 +4,22 @@
4
4
 
5
5
  <%= @language %>:
6
6
  <%
7
+ node_keys = []
7
8
  @segments.each do |term|
8
- if term.is_comment? %>
9
+ if term.is_comment? %>
9
10
  # <%= term.translation %>
10
- <% else %> <%= term.key %>: "<%= term.translation %>"
11
- <% end
11
+ <% else
12
+ if term.key == '[init-node]' or term.key == '[end-node]'
13
+ node_keys << term.translation if term.key == '[init-node]'
14
+ node_keys.pop if term.key == '[end-node]'
15
+ else
16
+ if node_keys.length() > 0
17
+ key_join = node_keys.join("_")+"_"+term.key
18
+ else
19
+ key_join = term.key
20
+ end
21
+ %> <%= key_join %>: "<%= term.translation %>"
22
+ <% end
12
23
  end
24
+ end
13
25
  %>
@@ -122,14 +122,26 @@
122
122
  <comment>Controls the Language and ensures that the font for all elements in the RootFrame aligns with the app's language. Set to the language code of this resource file's language.</comment>
123
123
  </data>
124
124
  <%
125
+ node_keys = []
125
126
  @segments.each do |term|
126
127
  if term.is_comment? %>
127
128
  <!-- <%= term.translation %> -->
128
- <% else %> <data name="<%= term.key %>" xml:space="preserve">
129
+ <% else
130
+ if term.key == '[init-node]' or term.key == '[end-node]'
131
+ node_keys << term.translation if term.key == '[init-node]'
132
+ node_keys.pop if term.key == '[end-node]'
133
+ else
134
+ if node_keys.length() > 0
135
+ key_join = node_keys.join().capitalize+term.key.capitalize
136
+ else
137
+ key_join = term.key
138
+ end
139
+ %> <data name="<%= key_join %>" xml:space="preserve">
129
140
  <value><![CDATA[<%= term.translation %>]]></value>
130
141
  <comment/>
131
142
  </data>
132
- <% end
143
+ <% end
144
+ end
133
145
  end
134
146
  %>
135
147
 
@@ -8,6 +8,19 @@ Created by localio
8
8
 
9
9
  import Foundation
10
10
 
11
- <% @segments.each do |term| %>
12
- let <%= term.key %>: String = { return NSLocalizedString("<%= term.translation %>", comment: "") }()<%
11
+ <%
12
+ node_keys = []
13
+ @segments.each do |term|
14
+ if term.key == '[init-node]' or term.key == '[end-node]'
15
+ node_keys << term.translation if term.key == '[init-node]'
16
+ node_keys.pop if term.key == '[end-node]'
17
+ else
18
+ if node_keys.length() >0
19
+ key_join = node_keys.join("_").capitalize+"_"+term.key.downcase
20
+ else
21
+ key_join = term.key
22
+ end
23
+ %>
24
+ let kLocale<%= key_join %>: String = { return NSLocalizedString("_<%= key_join %>", comment: "") }()<%
25
+ end
13
26
  end %>
@@ -1,3 +1,3 @@
1
1
  module Localio
2
- VERSION = "0.1.6"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -25,7 +25,7 @@ class IosWriter
25
25
 
26
26
  unless term.is_comment?
27
27
  constant_key = ios_constant_formatter term.keyword
28
- constant_value = key
28
+ constant_value = translation
29
29
  constant_segment = Segment.new(constant_key, constant_value, lang)
30
30
  constant_segments.segments << constant_segment
31
31
  end
@@ -45,11 +45,11 @@ class IosWriter
45
45
  private
46
46
 
47
47
  def self.ios_key_formatter(key)
48
- '_'+key.space_to_underscore.strip_tag.capitalize
48
+ key.space_to_underscore.strip_tag.capitalize
49
49
  end
50
50
 
51
51
  def self.ios_constant_formatter(key)
52
- 'kLocale'+key.space_to_underscore.strip_tag.camel_case
52
+ key.space_to_underscore.strip_tag.camel_case
53
53
  end
54
54
 
55
55
  end
@@ -25,7 +25,7 @@ class SwiftWriter
25
25
 
26
26
  unless term.is_comment?
27
27
  constant_key = swift_constant_formatter term.keyword
28
- constant_value = key
28
+ constant_value = translation
29
29
  constant_segment = Segment.new(constant_key, constant_value, lang)
30
30
  constant_segments.segments << constant_segment
31
31
  end
@@ -44,10 +44,10 @@ class SwiftWriter
44
44
  private
45
45
 
46
46
  def self.swift_key_formatter(key)
47
- '_'+key.space_to_underscore.strip_tag.capitalize
47
+ key.space_to_underscore.strip_tag.capitalize
48
48
  end
49
49
 
50
50
  def self.swift_constant_formatter(key)
51
- 'kLocale'+key.space_to_underscore.strip_tag.camel_case
51
+ key.space_to_underscore.strip_tag.camel_case
52
52
  end
53
53
  end
data/localio.gemspec CHANGED
@@ -1,35 +1,29 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'localio/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "localio"
8
- spec.version = Localio::VERSION
9
- spec.authors = ["Nacho Lopez"]
10
- spec.email = ["nacho@nlopez.io"]
11
- spec.description = %q{Automatic Localizable file generation for multiple platforms (Rails YAML, Android, Java Properties, iOS, JSON, .NET ResX)}
12
- spec.summary = %q{Automatic Localizable file generation for multiple type of files, like Android string.xml, Xcode Localizable.strings, JSON files, Rails YAML files, Java properties, etc. reading from Google Drive and Excel spreadsheets as base.}
13
- spec.homepage = "http://github.com/mrmans0n/localio"
14
- spec.license = "MIT"
6
+ spec.name = "localio"
7
+ spec.version = Localio::VERSION
8
+ spec.authors = ["Nacho Lopez"]
9
+ spec.email = ["nacho@nlopez.io"]
10
+ spec.description = %q{Automatic Localizable file generation for multiple platforms}
11
+ spec.summary = %q{Generates Android, iOS, Rails, JSON, Java Properties, and .NET ResX localization files from spreadsheet sources.}
12
+ spec.homepage = "https://github.com/mrmans0n/localio"
13
+ spec.license = "MIT"
15
14
 
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
15
+ spec.files = `git ls-files`.split("\n")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
17
  spec.require_paths = ["lib"]
20
18
 
21
- spec.executables << "localize"
19
+ spec.required_ruby_version = ">= 3.2"
22
20
 
23
- spec.add_development_dependency "rspec"
21
+ spec.add_development_dependency "rspec", "~> 3.0"
22
+ spec.add_development_dependency "rake", "~> 13.0"
24
23
 
25
- spec.required_ruby_version = ">= 1.9.2"
26
-
27
- spec.add_development_dependency "bundler", "~> 1.3"
28
- spec.add_development_dependency "rake"
29
-
30
- spec.add_dependency "micro-optparse", "~> 1.2"
31
- spec.add_dependency "google_drive", "~> 1.0"
32
- spec.add_dependency "spreadsheet", "~> 1.0"
33
- spec.add_dependency "simple_xlsx_reader", "~> 1.0"
34
- spec.add_dependency "nokogiri", "~> 1.6"
24
+ spec.add_dependency "google_drive", "~> 3.0"
25
+ spec.add_dependency "spreadsheet", "~> 1.3"
26
+ spec.add_dependency "simple_xlsx_reader", "~> 2.0"
27
+ spec.add_dependency "nokogiri", "~> 1.16"
28
+ spec.add_dependency "csv", "~> 3.2"
35
29
  end
@@ -0,0 +1,11 @@
1
+ Title Row,,,
2
+ [key],*en,es,fr
3
+ [comment],Section General,Section General,Section General
4
+ app_name,My App,Mi Aplicación,Mon Application
5
+ greeting,Hello %@ world,Hola %@ mundo,Bonjour %@ monde
6
+ dots_test,Wait...,Espera...,Attendez...
7
+ ampersand_test,Tom & Jerry,Tom & Jerry,Tom & Jerry
8
+ [init-node],module,module,module
9
+ nested_key,Module Key,Clave Módulo,Clé Module
10
+ [end-node],end,end,end
11
+ [end],,,
@@ -0,0 +1,40 @@
1
+ require 'localio/term'
2
+ require 'localio/filter'
3
+
4
+ RSpec.describe Filter do
5
+ let(:terms) do
6
+ ['app_name', 'app_title', 'settings_title', 'settings_back', '[comment]'].map { |kw| Term.new(kw) }
7
+ end
8
+
9
+ describe '.apply_filter' do
10
+ it 'returns all terms when no filters set' do
11
+ expect(Filter.apply_filter(terms, nil, nil)).to eq(terms)
12
+ end
13
+
14
+ context 'with only filter' do
15
+ it 'keeps terms matching the regex' do
16
+ result = Filter.apply_filter(terms, { keys: 'app_' }, nil)
17
+ expect(result.map(&:keyword)).to contain_exactly('app_name', 'app_title')
18
+ end
19
+
20
+ it 'returns empty array when nothing matches' do
21
+ expect(Filter.apply_filter(terms, { keys: 'nonexistent' }, nil)).to be_empty
22
+ end
23
+ end
24
+
25
+ context 'with except filter' do
26
+ it 'excludes terms matching the regex' do
27
+ result = Filter.apply_filter(terms, nil, { keys: 'settings_' })
28
+ expect(result.map(&:keyword)).not_to include('settings_title', 'settings_back')
29
+ expect(result.map(&:keyword)).to include('app_name', 'app_title')
30
+ end
31
+ end
32
+
33
+ context 'with both filters' do
34
+ it 'applies only first then except' do
35
+ result = Filter.apply_filter(terms, { keys: 'app_' }, { keys: 'title' })
36
+ expect(result.map(&:keyword)).to contain_exactly('app_name')
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/formatter'
3
+
4
+ RSpec.describe Formatter do
5
+ let(:smart_callback) { ->(key) { key.upcase } }
6
+
7
+ describe '.format' do
8
+ it ':smart delegates to callback' do
9
+ expect(Formatter.format('hello', :smart, smart_callback)).to eq('HELLO')
10
+ end
11
+
12
+ it ':none returns key unchanged' do
13
+ expect(Formatter.format('Hello World', :none, smart_callback)).to eq('Hello World')
14
+ end
15
+
16
+ it ':camel_case converts to CamelCase' do
17
+ expect(Formatter.format('hello world', :camel_case, smart_callback)).to eq('HelloWorld')
18
+ end
19
+
20
+ it ':camel_case strips single-letter bracket tags' do
21
+ expect(Formatter.format('[a]hello', :camel_case, smart_callback)).to eq('Hello')
22
+ end
23
+
24
+ it ':snake_case converts spaces to underscores and downcases' do
25
+ expect(Formatter.format('Hello World', :snake_case, smart_callback)).to eq('hello_world')
26
+ end
27
+
28
+ it 'raises ArgumentError for unknown formatter' do
29
+ expect { Formatter.format('key', :unknown, smart_callback) }.to raise_error(ArgumentError)
30
+ end
31
+ end
32
+ end