localio 0.1.7 → 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 (49) 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 +27 -34
  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/processors/csv_processor.rb +1 -1
  12. data/lib/localio/processors/google_drive_processor.rb +19 -45
  13. data/lib/localio/processors/xlsx_processor.rb +1 -1
  14. data/lib/localio/template_handler.rb +3 -1
  15. data/lib/localio/templates/android_localizable.erb +14 -2
  16. data/lib/localio/templates/ios_constant_localizable.erb +16 -2
  17. data/lib/localio/templates/ios_localizable.erb +20 -5
  18. data/lib/localio/templates/java_properties_localizable.erb +16 -2
  19. data/lib/localio/templates/json_localizable.erb +6 -5
  20. data/lib/localio/templates/rails_localizable.erb +15 -3
  21. data/lib/localio/templates/resx_localizable.erb +14 -2
  22. data/lib/localio/templates/swift_constant_localizable.erb +15 -2
  23. data/lib/localio/version.rb +1 -1
  24. data/lib/localio/writers/ios_writer.rb +3 -3
  25. data/lib/localio/writers/swift_writer.rb +3 -3
  26. data/localio.gemspec +19 -25
  27. data/spec/fixtures/sample.csv +11 -0
  28. data/spec/localio/filter_spec.rb +40 -0
  29. data/spec/localio/formatter_spec.rb +32 -0
  30. data/spec/localio/processors/csv_processor_spec.rb +89 -0
  31. data/spec/localio/processors/google_drive_processor_spec.rb +107 -0
  32. data/spec/localio/processors/xls_processor_spec.rb +65 -0
  33. data/spec/localio/processors/xlsx_processor_spec.rb +59 -0
  34. data/spec/localio/segment_spec.rb +27 -0
  35. data/spec/localio/segments_list_holder_spec.rb +22 -0
  36. data/spec/localio/string_helper_spec.rb +49 -0
  37. data/spec/localio/template_handler_spec.rb +67 -0
  38. data/spec/localio/term_spec.rb +24 -0
  39. data/spec/localio/writers/android_writer_spec.rb +71 -0
  40. data/spec/localio/writers/ios_writer_spec.rb +63 -0
  41. data/spec/localio/writers/java_properties_writer_spec.rb +35 -0
  42. data/spec/localio/writers/json_writer_spec.rb +57 -0
  43. data/spec/localio/writers/rails_writer_spec.rb +47 -0
  44. data/spec/localio/writers/resx_writer_spec.rb +44 -0
  45. data/spec/localio/writers/swift_writer_spec.rb +42 -0
  46. data/spec/localio_spec.rb +62 -0
  47. data/spec/spec_helper.rb +24 -0
  48. data/spec/support/shared_terms.rb +35 -0
  49. metadata +60 -49
@@ -42,7 +42,7 @@ class CsvProcessor
42
42
  unless platform_options[:avoid_lang_downcase]
43
43
  default_language = default_language.downcase
44
44
  lang = lang.downcase
45
- end
45
+ end
46
46
 
47
47
  unless col_text.to_s == ''
48
48
  languages.store lang, column
@@ -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
 
@@ -125,7 +99,7 @@ class GoogleDriveProcessor
125
99
  unless platform_options[:avoid_lang_downcase]
126
100
  default_language = default_language.downcase
127
101
  lang = lang.downcase
128
- end
102
+ end
129
103
 
130
104
  unless col_text.to_s == ''
131
105
  languages.store lang, column
@@ -49,7 +49,7 @@ class XlsxProcessor
49
49
  unless platform_options[:avoid_lang_downcase]
50
50
  default_language = default_language.downcase
51
51
  lang = lang.downcase
52
- end
52
+ end
53
53
 
54
54
  unless col_text.to_s == ''
55
55
  languages.store lang, column
@@ -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.7"
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
@@ -0,0 +1,89 @@
1
+ require 'localio/term'
2
+ require 'localio/string_helper'
3
+ require 'localio/processors/csv_processor'
4
+
5
+ RSpec.describe CsvProcessor do
6
+ let(:fixture_path) { File.expand_path('../../../fixtures/sample.csv', __FILE__) }
7
+ let(:platform_options) { {} }
8
+ let(:options) { { path: fixture_path } }
9
+
10
+ describe '.load_localizables' do
11
+ subject(:result) { CsvProcessor.load_localizables(platform_options, options) }
12
+
13
+ it 'returns languages en, es, fr' do
14
+ expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
15
+ end
16
+
17
+ it 'sets en as default language (marked with *)' do
18
+ expect(result[:default_language]).to eq('en')
19
+ end
20
+
21
+ it 'returns 8 terms between [key] and [end]' do
22
+ expect(result[:segments].count).to eq(8)
23
+ end
24
+
25
+ it 'parses term keywords correctly' do
26
+ keywords = result[:segments].map(&:keyword)
27
+ expect(keywords).to include('app_name', 'greeting', '[comment]', '[init-node]', '[end-node]')
28
+ end
29
+
30
+ it 'parses translations for each language' do
31
+ app_name = result[:segments].find { |t| t.keyword == 'app_name' }
32
+ expect(app_name.values['en']).to eq('My App')
33
+ expect(app_name.values['es']).to eq('Mi Aplicación')
34
+ expect(app_name.values['fr']).to eq('Mon Application')
35
+ end
36
+
37
+ it 'identifies comment rows' do
38
+ comment = result[:segments].find { |t| t.keyword == '[comment]' }
39
+ expect(comment.is_comment?).to be true
40
+ end
41
+
42
+ it 'raises ArgumentError when :path is missing' do
43
+ expect { CsvProcessor.load_localizables({}, {}) }
44
+ .to raise_error(ArgumentError, /:path attribute is missing/)
45
+ end
46
+
47
+ it 'raises IndexError when [key] marker is missing' do
48
+ Dir.mktmpdir do |tmpdir|
49
+ path = File.join(tmpdir, 'bad.csv')
50
+ File.write(path, "no,key,row\ndata,here,\n[end],,,\n")
51
+ expect { CsvProcessor.load_localizables({}, { path: path }) }
52
+ .to raise_error(IndexError, /Could not find any \[key\]/)
53
+ end
54
+ end
55
+
56
+ it 'raises IndexError when [end] marker is missing' do
57
+ Dir.mktmpdir do |tmpdir|
58
+ path = File.join(tmpdir, 'bad.csv')
59
+ File.write(path, "title,,,\n[key],*en,es,\ndata,val,val,\n")
60
+ expect { CsvProcessor.load_localizables({}, { path: path }) }
61
+ .to raise_error(IndexError, /Could not find any \[end\]/)
62
+ end
63
+ end
64
+
65
+ context 'with override_default option' do
66
+ let(:platform_options) { { override_default: 'es' } }
67
+
68
+ it 'uses the overridden default language' do
69
+ expect(result[:default_language]).to eq('es')
70
+ end
71
+ end
72
+
73
+ context 'with avoid_lang_downcase option' do
74
+ let(:tmpdir) { Dir.mktmpdir }
75
+ let(:options) do
76
+ path = File.join(tmpdir, 'upper.csv')
77
+ File.write(path, "Title,,,\n[key],*EN,ES,FR\napp_name,My App,Mi App,Mon App\n[end],,,\n")
78
+ { path: path }
79
+ end
80
+ let(:platform_options) { { avoid_lang_downcase: true } }
81
+
82
+ after { FileUtils.rm_rf(tmpdir) }
83
+
84
+ it 'preserves language case' do
85
+ expect(result[:languages].keys).to contain_exactly('EN', 'ES', 'FR')
86
+ end
87
+ end
88
+ end
89
+ end