i18n_uno 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +34 -0
- data/LICENSE +21 -0
- data/README.md +90 -0
- data/Rakefile +8 -0
- data/i18n_uno.gemspec +33 -0
- data/lib/.DS_Store +0 -0
- data/lib/i18n_uno/.DS_Store +0 -0
- data/lib/i18n_uno/arbiter.rb +87 -0
- data/lib/i18n_uno/comparer.rb +61 -0
- data/lib/i18n_uno/configuration.rb +25 -0
- data/lib/i18n_uno/delta.rb +62 -0
- data/lib/i18n_uno/file.rb +41 -0
- data/lib/i18n_uno/files/content_change.rb +149 -0
- data/lib/i18n_uno/open_ai_client.rb +32 -0
- data/lib/i18n_uno/railtie.rb +12 -0
- data/lib/i18n_uno/translator.rb +53 -0
- data/lib/i18n_uno/tree.rb +106 -0
- data/lib/i18n_uno/version.rb +5 -0
- data/lib/i18n_uno.rb +25 -0
- data/lib/tasks/i18n_uno.rake +15 -0
- data/rubocop.yml +240 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a96558cb92d39d42fcaafb70c1ff3b9ca633cc2d4fb4849bb7bf7243ea09954e
|
4
|
+
data.tar.gz: eab75bee802e842847a77288aaec1dfc7bb1ee9785bdf290687c973249d6dd40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ac9a40dbfad9a8b5ef8e29a3fb69f78c9d0fd5311691a240c0a9227f93606ac536e65afce3bead3820c39f3d9778eb0663ebb5acdb66df302605fd7f35e9c4a7
|
7
|
+
data.tar.gz: 0ef6c4fcbd31728906dfe936dbb55af29a8165e09a5e1096be623b8cc302b2c4a90701dcacd53b5170ad1ae874af8555d2066a2cfade60a631dda9c7e05c5216
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
i18n_uno (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.5.1)
|
10
|
+
rake (10.5.0)
|
11
|
+
rspec (3.13.0)
|
12
|
+
rspec-core (~> 3.13.0)
|
13
|
+
rspec-expectations (~> 3.13.0)
|
14
|
+
rspec-mocks (~> 3.13.0)
|
15
|
+
rspec-core (3.13.0)
|
16
|
+
rspec-support (~> 3.13.0)
|
17
|
+
rspec-expectations (3.13.0)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.13.0)
|
20
|
+
rspec-mocks (3.13.0)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.13.0)
|
23
|
+
rspec-support (3.13.1)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
i18n_uno!
|
30
|
+
rake (~> 10.0)
|
31
|
+
rspec (~> 3.0)
|
32
|
+
|
33
|
+
BUNDLED WITH
|
34
|
+
1.17.3
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 pythagora-north
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# i18nUno
|
2
|
+
|
3
|
+
i18nUno is a gem that helps you translate your Rails application into any language you choose. It utilizes the OpenAI API for translations and supports all languages. It's easy to use, and by implementing Git hooks, you can set it up to automatically add new translations or delete old ones as a pre-commit hook.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
group :development do
|
11
|
+
...
|
12
|
+
gem 'i18n_uno'
|
13
|
+
end
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle install
|
19
|
+
|
20
|
+
## Prerequisites
|
21
|
+
|
22
|
+
To use the gem, adhere to the standard procedures outlined in the [Rails I18n support guide](https://guides.rubyonrails.org/i18n.html). While you may organize your folders as you wish, all localization files must be named following the `$locale.yml` format, such as `en.yml` for English. The i18n Uno gem will leverage these files to generate new ones for additional languages you wish to include, for instance, `es.yml` for Spanish, `de.yml` for German etc.
|
23
|
+
|
24
|
+
## Configuration
|
25
|
+
|
26
|
+
To utilize the gem, create a file `config/initializers/i18n_uno.rb` and configure the gem with your OpenAI credentials. Additional settings are available for customization.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
I18nUno.configure do |config|
|
30
|
+
config.open_ai_key = ENV['OPEN_AI_API_KEY'] # Required: Found at https://platform.openai.com/account/api-keys
|
31
|
+
config.default_locale # Required: You can set it to I18n.config.default_locale
|
32
|
+
config.available_locales # Required: I18n.config.default_locale and locales you want to translate to
|
33
|
+
config.open_ai_model # Optional: Defaults to 'gpt-4-0613'
|
34
|
+
config.load_path # Optional: Defaults to 'config/locales'
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Although changing the model is supported, it is strongly recommended to stick with gpt-4 models due to the significant quality difference compared to gpt-3 models.
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
Before running the gem, ensure you've added the desired languages to `config.available_locales` or in the `config/application.rb` file.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
config.i18n.available_locales = [:en, :bs, :de]
|
46
|
+
```
|
47
|
+
|
48
|
+
With these settings, translations will be executed to add all necessary files to support these languages. From this point, simply run:
|
49
|
+
|
50
|
+
```bash
|
51
|
+
rake i18n_uno:translate
|
52
|
+
```
|
53
|
+
|
54
|
+
from your application folder. i18n Uno will then add new translation files, and you'll be ready to go. If new keys are added during development, running the above command again will automatically add and translate these new keys. Key removal is also supported and will be propagated to all language files.
|
55
|
+
|
56
|
+
## Continuous Management of Internationalization
|
57
|
+
|
58
|
+
Your default locale serves as the foundational reference for your application's internationalization. Whenever modifications are made to the localization files, executing the previously mentioned gem command will automatically update translations across all other supported languages, adding or removing them as necessary.
|
59
|
+
|
60
|
+
To ensure seamless integration of this process, it's advisable to configure a pre-commit hook that triggers the above command. This step helps maintain consistency by preventing the introduction of new keys without their corresponding translations.
|
61
|
+
|
62
|
+
It's important to note that the gem is designed to respect the integrity of your source of truth files (the default locale) and, as such, will not alter these files directly. Additionally, it does not track changes to existing keys, focusing instead on the addition or removal of translations based on the current state of your default locale files.
|
63
|
+
|
64
|
+
## Setting up application for internationalization
|
65
|
+
|
66
|
+
If you are not already supporting internationalization in your rails application you can do that simply by adding `locale` field to your `user` model.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class AddLocaleToUsers < ActiveRecord::Migration
|
70
|
+
def change
|
71
|
+
add_column :users, :locale, :string, default: 'en'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
From there adding you simply need to add before action to `app/controllers/authenticated_controller.rb`. This would very based on your application but this would be the standard way.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
before_action :set_locale!
|
80
|
+
|
81
|
+
def set_locale!
|
82
|
+
I18n.locale = current_user.locale
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
That would be it, magic of rails will take care of the rest.
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/i18n_uno.
|
data/Rakefile
ADDED
data/i18n_uno.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'i18n_uno/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'i18n_uno'
|
9
|
+
spec.version = I18nUno::VERSION
|
10
|
+
spec.authors = ['Haris Krajina', 'hkraji']
|
11
|
+
spec.email = ['haris@pythagoranorth.com']
|
12
|
+
|
13
|
+
spec.summary = 'i18n Uno levrages power of ChatGPT API to translate your i18n files.'
|
14
|
+
spec.description = 'i18n Uno is simple CLI tool that will completly translate your application to any desired language.'
|
15
|
+
spec.homepage = 'https://github.com/pythagora-north/i18n_uno'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/pythagora-north/i18n_uno'
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/pythagora-north/i18n_uno/blob/main/Changes.md'
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
end
|
27
|
+
spec.bindir = 'exe'
|
28
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ['lib']
|
30
|
+
|
31
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
32
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
33
|
+
end
|
data/lib/.DS_Store
ADDED
Binary file
|
Binary file
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nUno
|
4
|
+
class Arbiter
|
5
|
+
attr_reader :translators, :trees, :source_of_truth_tree, :target_locales
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@translators = []
|
9
|
+
@trees = {}
|
10
|
+
|
11
|
+
setup_configurations
|
12
|
+
end
|
13
|
+
|
14
|
+
def translate
|
15
|
+
print_translations_info
|
16
|
+
|
17
|
+
target_locales.each do |target_locale|
|
18
|
+
collect_locale_changes(target_locale)
|
19
|
+
process_changes(target_locale)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def setup_configurations
|
26
|
+
check_locale_availability
|
27
|
+
setup_signal_handling
|
28
|
+
setup_target_locales
|
29
|
+
setup_translation_trees
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_locale_availability
|
33
|
+
if I18nUno.config.available_locales.nil?
|
34
|
+
raise I18nUno::ConfigurationError, 'No available locales specified. Please set the available locales in the configuration file.'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def setup_target_locales
|
39
|
+
@target_locales = I18nUno.config.available_locales - [I18nUno.config.default_locale]
|
40
|
+
end
|
41
|
+
|
42
|
+
def setup_translation_trees
|
43
|
+
@target_locales.each { |locale| @trees[locale] = I18nUno::Tree.new(locale) }
|
44
|
+
@source_of_truth_tree = I18nUno::Tree.new(I18nUno.config.default_locale)
|
45
|
+
end
|
46
|
+
|
47
|
+
def collect_locale_changes(target_locale)
|
48
|
+
puts "\nChecking for translation changes for locale '#{target_locale}'\n"
|
49
|
+
|
50
|
+
source_of_truth_tree.each do |sot_file|
|
51
|
+
target_file = @trees[target_locale].find_or_create_file(sot_file)
|
52
|
+
|
53
|
+
comparer = I18nUno::Comparer.new(sot_file, target_file)
|
54
|
+
diff_delta = comparer.compare
|
55
|
+
@trees[target_locale].add_delta!(target_file, diff_delta)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_changes(target_locale)
|
60
|
+
@trees[target_locale].process_delta_changes(source_of_truth_tree)
|
61
|
+
end
|
62
|
+
|
63
|
+
def setup_signal_handling
|
64
|
+
Signal.trap('SIGINT') do
|
65
|
+
puts "\nExiting I18n Uno ..."
|
66
|
+
target_locales.each do |target_locale|
|
67
|
+
@trees[target_locale].clean_not_processed_files
|
68
|
+
end
|
69
|
+
exit
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def print_translations_info
|
74
|
+
if source_of_truth_tree.empty?
|
75
|
+
puts "No translation files found in the specified path (#{I18nUno.config.load_path}}/**/#{I18nUno.config.default_locale}.yml). Please check the path and try again."
|
76
|
+
exit(0)
|
77
|
+
else
|
78
|
+
puts "Listing translation files for the default locale '#{I18nUno.config.default_locale}':\n\n"
|
79
|
+
source_of_truth_tree.files.each_slice(2) do |file_pair|
|
80
|
+
file_pair.map! { |file| file.file_path.gsub(%r{#{I18nUno.config.load_path}/?}, '') }
|
81
|
+
print_string = file_pair.size.even? ? "- %-40s - %-40s\n" : "- %-40s\n"
|
82
|
+
printf(print_string, *file_pair)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nUno
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class Comparer
|
7
|
+
attr_accessor :source_file, :target_file
|
8
|
+
|
9
|
+
def initialize(source_file, target_file)
|
10
|
+
@source_file = source_file
|
11
|
+
@target_file = target_file
|
12
|
+
end
|
13
|
+
|
14
|
+
def compare
|
15
|
+
source_keys = flatten_keys(normalize_data(source_file.content_hash))
|
16
|
+
target_keys = flatten_keys(normalize_data(target_file.content_hash))
|
17
|
+
|
18
|
+
new_keys = compare_keys(target_keys, source_keys)
|
19
|
+
removed_keys = compare_keys(source_keys, target_keys)
|
20
|
+
|
21
|
+
I18nUno::Delta.new(
|
22
|
+
new_keys: new_keys,
|
23
|
+
removed_keys: removed_keys,
|
24
|
+
target_keys_size: target_keys.size
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def compare_keys(source_keys, target_keys)
|
31
|
+
target_keys - source_keys
|
32
|
+
end
|
33
|
+
|
34
|
+
# Flattens the nested hash of translation keys into a flat array of dot-prefixed keys.
|
35
|
+
# @param data [Hash] the translation file content
|
36
|
+
# @param prefix [String] the current prefix to prepend to keys (used in recursion)
|
37
|
+
# @return [Array] an array of flattened key strings
|
38
|
+
def flatten_keys(data, prefix = '')
|
39
|
+
data.each_with_object([]) do |(key, value), keys|
|
40
|
+
full_key = "#{prefix}#{key}"
|
41
|
+
if value.is_a?(Hash)
|
42
|
+
keys.concat(flatten_keys(value, "#{full_key}."))
|
43
|
+
else
|
44
|
+
keys << full_key unless value.nil?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# Removes the first key for the data, since that is locale for that file content
|
51
|
+
# @param data [Hash] the translation file content
|
52
|
+
# @return [Hash] the translation file content without the locale key
|
53
|
+
def normalize_data(data)
|
54
|
+
if data.keys.first.length == 2 && data.keys.length == 1
|
55
|
+
data[data.keys.first]
|
56
|
+
else
|
57
|
+
data
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nUno
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :load_path, :open_ai_model, :open_ai_key, :default_locale, :available_locales
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
raise ConfigurationError, 'Rails must be present to use I18nUno' unless defined?(Rails)
|
9
|
+
|
10
|
+
@load_path = ::File.join(Rails.root, 'config', 'locales')
|
11
|
+
@open_ai_model = 'gpt-4-0613'
|
12
|
+
@open_ai_key = nil
|
13
|
+
@default_locale = nil
|
14
|
+
@available_locales = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate!
|
18
|
+
raise ConfigurationError, 'open_ai_key must be set for API operations' unless open_ai_key
|
19
|
+
raise ConfigurationError, 'available_locales must be set' unless available_locales
|
20
|
+
raise ConfigurationError, 'default_locale must be included in available_locales' if available_locales && !available_locales.include?(default_locale)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ConfigurationError < StandardError; end
|
25
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nUno
|
4
|
+
class Delta
|
5
|
+
attr_accessor :new_keys, :removed_keys, :values, :translated_values, :target_file
|
6
|
+
|
7
|
+
# @param new_keys [Array<String>] keys that are newly added.
|
8
|
+
# @param removed_keys [Array<String>] keys that have been removed.
|
9
|
+
# @param target_keys_size [Integer] the total number of keys in the target file.
|
10
|
+
def initialize(new_keys: [], removed_keys: [], target_keys_size: 0)
|
11
|
+
@new_keys = new_keys
|
12
|
+
@removed_keys = removed_keys
|
13
|
+
@values = []
|
14
|
+
@translated_values = []
|
15
|
+
@complete_file_diff = calculate_complete_file_diff(new_keys, target_keys_size)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Calculates if the delta represents a complete difference of a file.
|
19
|
+
# @return [Boolean] true if the new keys are equal to the total number of keys in the target.
|
20
|
+
def calculate_complete_file_diff(new_keys, target_keys_size)
|
21
|
+
new_keys.any? && new_keys.size == target_keys_size
|
22
|
+
end
|
23
|
+
|
24
|
+
# Determines if the entire file has been changed based on the delta.
|
25
|
+
# @return [Boolean] true if all keys in the file are considered new.
|
26
|
+
def complete_file_diff?
|
27
|
+
@complete_file_diff
|
28
|
+
end
|
29
|
+
|
30
|
+
# Checks if there are any changes (new or removed keys).
|
31
|
+
# @return [Boolean] true if there are any new or removed keys.
|
32
|
+
def any_changes?
|
33
|
+
any_new_keys? || any_removed_keys?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks if there are any new keys in the delta.
|
37
|
+
# @return [Boolean] true if there are new keys present.
|
38
|
+
def any_new_keys?
|
39
|
+
new_keys.any?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Checks if there are any removed keys in the delta.
|
43
|
+
# @return [Boolean] true if there are keys that have been removed.
|
44
|
+
def any_removed_keys?
|
45
|
+
removed_keys.any?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Determines if there are any values ready to be translated.
|
49
|
+
# This checks for the presence of non-nil entries in the values list.
|
50
|
+
# @return [Boolean] true if there are non-nil values to translate.
|
51
|
+
def anything_to_translate?
|
52
|
+
values.compact.any?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Populates the values list from a specified file based on the new keys.
|
56
|
+
# This is intended to gather the actual values to be translated.
|
57
|
+
# @param file [Object] typically an instance of a File class, used to extract values.
|
58
|
+
def values_from!(file)
|
59
|
+
@values = file.values_from_keys(new_keys)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'i18n_uno/files/content_change'
|
5
|
+
|
6
|
+
module I18nUno
|
7
|
+
class File
|
8
|
+
include I18nUno::Files::ContentChange
|
9
|
+
|
10
|
+
attr_accessor :content_hash
|
11
|
+
attr_reader :file_path, :locale
|
12
|
+
|
13
|
+
def initialize(file_path)
|
14
|
+
@file_path = file_path
|
15
|
+
|
16
|
+
@locale = ::File.basename(file_path, '.yml')
|
17
|
+
@content_hash = YAML.load_file(file_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def pp_file_name
|
21
|
+
file_path.gsub(%r{#{I18nUno.config.load_path}/?}, '')
|
22
|
+
end
|
23
|
+
|
24
|
+
def file_identifier
|
25
|
+
file_path.gsub(%r{/#{locale}\.yml$}, '')
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_in_locale(locale, save_file = false)
|
29
|
+
new_path = path_in_locale(locale)
|
30
|
+
FileUtils.cp(file_path, path_in_locale(locale))
|
31
|
+
|
32
|
+
file = I18nUno::File.new(new_path)
|
33
|
+
file.setup_new_file(save_file)
|
34
|
+
file
|
35
|
+
end
|
36
|
+
|
37
|
+
def path_in_locale(locale)
|
38
|
+
new_path = file_path.gsub(%r{/#{self.locale}\.yml$}, "/#{locale}.yml")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nUno
|
4
|
+
module Files
|
5
|
+
module ContentChange
|
6
|
+
# Reorders keys in the file to match the order of the source file.
|
7
|
+
# @param sot_file [I18nUno::File] The source of truth file with the desired key order.
|
8
|
+
def match_order_as_file!(sot_file)
|
9
|
+
ordered_hash = {}
|
10
|
+
ordered_hash[locale] = merge_preserving_key_order(
|
11
|
+
sot_file.content_hash.values.first,
|
12
|
+
content_hash.values.first
|
13
|
+
)
|
14
|
+
|
15
|
+
@content_hash = ordered_hash
|
16
|
+
end
|
17
|
+
|
18
|
+
# Adds missing keys to the target file and initializes them to nil.
|
19
|
+
# @param delta [I18nUno::Delta] Contains the new keys to be added.
|
20
|
+
def add_missing_keys_to_target_file!(delta)
|
21
|
+
delta.new_keys.each do |key_path|
|
22
|
+
keys = key_path.split('.').unshift(locale)
|
23
|
+
deep_set_to_nil!(content_hash, keys)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieves values from the file using dot-separated keys.
|
28
|
+
# @param dot_keys [Array<String>] Array of keys to retrieve values for.
|
29
|
+
# @return [Array] The values corresponding to the dot_keys.
|
30
|
+
def values_from_keys(dot_keys)
|
31
|
+
dot_keys.collect do |dot_keys|
|
32
|
+
keys = dot_keys.split('.').unshift(locale)
|
33
|
+
value = content_hash
|
34
|
+
|
35
|
+
loop do
|
36
|
+
value = value[keys.shift]
|
37
|
+
break if keys.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Updates the file with values from translated deltas.
|
45
|
+
# @param delta [I18nUno::Delta] Contains the keys and their translated values.
|
46
|
+
def set_values_from_delta!(delta)
|
47
|
+
delta.new_keys.each_with_index do |key_path, index|
|
48
|
+
keys = key_path.split('.').unshift(locale)
|
49
|
+
value = content_hash
|
50
|
+
|
51
|
+
loop do
|
52
|
+
current_key = keys.shift
|
53
|
+
|
54
|
+
if keys.empty?
|
55
|
+
value[current_key] = delta.translated_values[index]
|
56
|
+
break
|
57
|
+
else
|
58
|
+
value = value[current_key]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Removes specified keys from the file.
|
65
|
+
# @param delta [I18nUno::Delta] Contains the keys to be removed.
|
66
|
+
def remove_keys_from_file!(delta)
|
67
|
+
delta.removed_keys.each do |key_path|
|
68
|
+
keys = key_path.split('.').unshift(locale)
|
69
|
+
deep_key_remove!(content_hash, keys)
|
70
|
+
end
|
71
|
+
save!
|
72
|
+
end
|
73
|
+
|
74
|
+
# Saves the file to HDD
|
75
|
+
def save!
|
76
|
+
::File.open(file_path, 'w') do |file|
|
77
|
+
yaml_content = content_hash.to_yaml(options: { line_width: -1 })
|
78
|
+
yaml_content.sub!(/\A---\s*\n/, '')
|
79
|
+
file.write(yaml_content)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Prepares a new file with all primitive values set to nil.
|
84
|
+
# @param save_file [Boolean] Determines if the file should be saved immediately after setup.
|
85
|
+
def setup_new_file(save_file)
|
86
|
+
original_locale = @content_hash.keys.first
|
87
|
+
@content_hash[locale] = @content_hash.delete(original_locale)
|
88
|
+
set_primitives_to_nil!(@content_hash)
|
89
|
+
save! if save_file
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Recursively sets all primitive values in a hash to nil.
|
95
|
+
# @param hash [Hash] The hash to modify.
|
96
|
+
def set_primitives_to_nil!(hash)
|
97
|
+
hash.each do |key, value|
|
98
|
+
if value.is_a?(Hash)
|
99
|
+
set_primitives_to_nil!(value)
|
100
|
+
else
|
101
|
+
hash[key] = nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Merges two hashes preserving the order of keys from the source hash.
|
107
|
+
# @param source_hash [Hash] The source hash defining the order of keys.
|
108
|
+
# @param target_hash [Hash] The target hash to merge.
|
109
|
+
# @return [Hash] The merged hash with preserved key order.
|
110
|
+
def merge_preserving_key_order(source_hash, target_hash)
|
111
|
+
merged_hash = {}
|
112
|
+
|
113
|
+
source_hash.each_key do |key|
|
114
|
+
merged_hash[key] = if source_hash[key].is_a?(Hash) && target_hash[key].is_a?(Hash)
|
115
|
+
merge_preserving_key_order(source_hash[key], target_hash[key])
|
116
|
+
else
|
117
|
+
target_hash[key]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
merged_hash
|
122
|
+
end
|
123
|
+
|
124
|
+
# Recursively removes a key from a nested hash.
|
125
|
+
# @param obj [Hash] The hash from which to remove the key.
|
126
|
+
# @param keys [Array<String>] The path of keys leading to the key to remove.
|
127
|
+
def deep_key_remove!(obj, keys)
|
128
|
+
first_key = keys.shift
|
129
|
+
|
130
|
+
if keys.empty?
|
131
|
+
obj.delete(first_key)
|
132
|
+
else
|
133
|
+
deep_key_remove!(obj[first_key], keys)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Sets all leaf nodes of a nested hash to nil.
|
138
|
+
# @param obj [Hash] The hash to modify.
|
139
|
+
# @param keys [Array<String>] The path of keys.
|
140
|
+
def deep_set_to_nil!(obj, keys)
|
141
|
+
last_key = keys.pop
|
142
|
+
last_hash = keys.inject(obj) do |o, k|
|
143
|
+
o[k] ||= {}
|
144
|
+
end
|
145
|
+
last_hash[last_key] = nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nUno
|
4
|
+
class OpenAIClient
|
5
|
+
def initialize
|
6
|
+
@connection = Faraday.new(
|
7
|
+
url: 'https://api.openai.com',
|
8
|
+
headers: { 'Authorization' => "Bearer #{I18nUno.config.open_ai_key}",
|
9
|
+
'Content-Type' => 'application/json; charset=utf-8' }
|
10
|
+
) do |faraday|
|
11
|
+
faraday.adapter Faraday.default_adapter
|
12
|
+
faraday.options[:timeout] = 600
|
13
|
+
faraday.options[:open_timeout] = 30
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def complete(messages:)
|
18
|
+
response = @connection.post('/v1/chat/completions') do |request|
|
19
|
+
request.body = {
|
20
|
+
model: I18nUno.config.open_ai_model,
|
21
|
+
messages: messages.map { |message| { role: :user, content: message } },
|
22
|
+
max_tokens: 4096
|
23
|
+
}.to_json
|
24
|
+
end
|
25
|
+
|
26
|
+
JSON.parse(response.body)['choices'].first['message']['content']
|
27
|
+
rescue StandardError => e
|
28
|
+
Rails.logger.error(e)
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'i18n_uno/open_ai_client'
|
5
|
+
|
6
|
+
module I18nUno
|
7
|
+
class Translator
|
8
|
+
attr_reader :source_file, :target_file
|
9
|
+
|
10
|
+
def initialize(sot_file, target_file)
|
11
|
+
@source_file = sot_file
|
12
|
+
@target_file = target_file
|
13
|
+
@target_file_yaml = YAML.load_file(target_file.file_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def process(delta)
|
17
|
+
delta.values_from!(@source_file)
|
18
|
+
|
19
|
+
if delta.anything_to_translate?
|
20
|
+
delta = send_to_chatgpt_api(delta)
|
21
|
+
|
22
|
+
target_file.add_missing_keys_to_target_file!(delta)
|
23
|
+
target_file.match_order_as_file!(source_file)
|
24
|
+
target_file.set_values_from_delta!(delta)
|
25
|
+
target_file.save!
|
26
|
+
else
|
27
|
+
puts 'Missing keys are all null, nothing to translate'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def send_to_chatgpt_api(delta)
|
34
|
+
values_to_translate = delta.values
|
35
|
+
client = OpenAIClient.new
|
36
|
+
translated_values = []
|
37
|
+
|
38
|
+
values_to_translate.each_slice(200) do |chunk|
|
39
|
+
messages = ["Please translate the following list of strings from the source language specified by '#{source_file.locale}' to the target language indicated by '#{target_file.locale}'. The translations should be suitable for use in a user interface, taking into account that shorter strings are likely to be used as button labels or element captions. The output should be in the form of an array containing the translated messages, ensuring that each translated value is accurate and correctly formatted for JSON parsing. Special attention should be paid to maintaining the context and usability of each UI element in the target language. Below is the list for translation:"]
|
40
|
+
|
41
|
+
messages << chunk.to_json
|
42
|
+
|
43
|
+
response = client.complete(messages: messages)
|
44
|
+
chunk_translated_values = JSON.parse(response)
|
45
|
+
|
46
|
+
translated_values.concat(chunk_translated_values)
|
47
|
+
end
|
48
|
+
|
49
|
+
delta.translated_values = translated_values
|
50
|
+
delta
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nUno
|
4
|
+
class Tree
|
5
|
+
attr_reader :locale
|
6
|
+
|
7
|
+
def initialize(locale)
|
8
|
+
@locale = locale
|
9
|
+
@data = [] # [{ file: I18nUno::File, delta: I18nUno::DiffDelta }]
|
10
|
+
collect_files
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_delta_changes(source_of_truth_tree)
|
14
|
+
entries_with_delta = extract_entries_with_delta
|
15
|
+
|
16
|
+
if entries_with_delta.empty?
|
17
|
+
puts "No changes detected for '#{locale}' locale."
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
files_completed = 0
|
22
|
+
puts "Processing locale '#{locale}' there are #{entries_with_delta.count} files with changes. "
|
23
|
+
entries_with_delta.each_with_index do |entry, index|
|
24
|
+
printf("Processing file %-40s [#{index + 1}/#{entries_with_delta.count}] ... ", "'#{entry[:file].pp_file_name}'")
|
25
|
+
|
26
|
+
if entry[:delta].any_new_keys?
|
27
|
+
sot_file = source_of_truth_tree.find_file(entry[:file])
|
28
|
+
translator = I18nUno::Translator.new(sot_file, entry[:file])
|
29
|
+
translator.process(entry[:delta])
|
30
|
+
end
|
31
|
+
|
32
|
+
if entry[:delta].any_removed_keys? && !entry[:delta].complete_file_diff?
|
33
|
+
entry[:file].remove_keys_from_file!(entry[:delta])
|
34
|
+
end
|
35
|
+
|
36
|
+
entry[:processed] = true
|
37
|
+
|
38
|
+
puts 'OK'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_entries_with_delta
|
43
|
+
@data.select do |entry|
|
44
|
+
entry[:delta].present? && entry[:delta].any_changes?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_delta!(file, diff_delta)
|
49
|
+
@data.each do |entry|
|
50
|
+
if entry[:file] == file
|
51
|
+
entry[:delta] = diff_delta
|
52
|
+
break
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def files
|
58
|
+
@data.map { |entry| entry[:file] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def any?
|
62
|
+
@data.any?
|
63
|
+
end
|
64
|
+
|
65
|
+
def empty?
|
66
|
+
@data.empty?
|
67
|
+
end
|
68
|
+
|
69
|
+
def each(&block)
|
70
|
+
files.each(&block)
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_file(target_file)
|
74
|
+
files.find { |file| file.file_path.match(/#{target_file.file_identifier}/) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_or_create_file(sot_file)
|
78
|
+
file = find_file(sot_file)
|
79
|
+
|
80
|
+
if file.nil?
|
81
|
+
file = sot_file.create_in_locale(locale)
|
82
|
+
@data << { file: file, delta: nil, new_file: true, processed: false }
|
83
|
+
end
|
84
|
+
|
85
|
+
file
|
86
|
+
end
|
87
|
+
|
88
|
+
def clean_not_processed_files
|
89
|
+
@data.each do |entry|
|
90
|
+
if (entry[:new_file] && !entry[:processed])
|
91
|
+
::File.delete(entry[:file].file_path)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def collect_files
|
100
|
+
path_pattern = ::File.join(I18nUno.config.load_path, '**', "#{locale}.yml")
|
101
|
+
Dir.glob(path_pattern).map do |file_path|
|
102
|
+
@data << { file: I18nUno::File.new(file_path), delta: nil }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/i18n_uno.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n_uno/version'
|
4
|
+
require 'i18n_uno/configuration'
|
5
|
+
require 'i18n_uno/railtie' if defined?(Rails)
|
6
|
+
require 'i18n_uno/arbiter'
|
7
|
+
require 'i18n_uno/comparer'
|
8
|
+
require 'i18n_uno/translator'
|
9
|
+
require 'i18n_uno/tree'
|
10
|
+
require 'i18n_uno/file'
|
11
|
+
require 'i18n_uno/delta'
|
12
|
+
|
13
|
+
module I18nUno
|
14
|
+
class << self
|
15
|
+
attr_accessor :config
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.config
|
19
|
+
@config ||= I18nUno::Configuration.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configure
|
23
|
+
yield(config)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :i18n_uno do
|
4
|
+
desc 'Translate all your I18n files'
|
5
|
+
task translate: :environment do
|
6
|
+
begin
|
7
|
+
I18nUno.config.validate!
|
8
|
+
|
9
|
+
i18n_uno = I18nUno::Arbiter.new
|
10
|
+
i18n_uno.translate
|
11
|
+
rescue I18nUno::ConfigurationError => e
|
12
|
+
puts("#{e.class}: #{e.message}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/rubocop.yml
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
- rubocop-rails
|
4
|
+
- rubocop-minitest
|
5
|
+
|
6
|
+
inherit_mode:
|
7
|
+
merge:
|
8
|
+
- Exclude
|
9
|
+
|
10
|
+
AllCops:
|
11
|
+
SuggestExtensions: false
|
12
|
+
DisabledByDefault: true
|
13
|
+
Exclude:
|
14
|
+
- "data/**/*"
|
15
|
+
|
16
|
+
# Align `when` with `end`.
|
17
|
+
Layout/CaseIndentation:
|
18
|
+
Enabled: true
|
19
|
+
EnforcedStyle: end
|
20
|
+
|
21
|
+
# Align comments with method definitions.
|
22
|
+
Layout/CommentIndentation:
|
23
|
+
Enabled: true
|
24
|
+
|
25
|
+
Layout/ElseAlignment:
|
26
|
+
Enabled: true
|
27
|
+
|
28
|
+
Layout/EmptyLineAfterMagicComment:
|
29
|
+
Enabled: true
|
30
|
+
|
31
|
+
Layout/EmptyLinesAroundBlockBody:
|
32
|
+
Enabled: true
|
33
|
+
|
34
|
+
# In a regular class definition, no empty lines around the body.
|
35
|
+
Layout/EmptyLinesAroundClassBody:
|
36
|
+
Enabled: true
|
37
|
+
|
38
|
+
# In a regular method definition, no empty lines around the body.
|
39
|
+
Layout/EmptyLinesAroundMethodBody:
|
40
|
+
Enabled: true
|
41
|
+
|
42
|
+
# In a regular module definition, no empty lines around the body.
|
43
|
+
Layout/EmptyLinesAroundModuleBody:
|
44
|
+
Enabled: true
|
45
|
+
|
46
|
+
# Align `end` with the matching keyword or starting expression except for
|
47
|
+
# assignments, where it should be aligned with the LHS.
|
48
|
+
Layout/EndAlignment:
|
49
|
+
Enabled: true
|
50
|
+
EnforcedStyleAlignWith: variable
|
51
|
+
AutoCorrect: true
|
52
|
+
|
53
|
+
# Method definitions after `private` or `protected` isolated calls need one
|
54
|
+
# extra level of indentation.
|
55
|
+
#
|
56
|
+
# We break this rule in context, though, e.g. for private-only concerns,
|
57
|
+
# so we leave it disabled.
|
58
|
+
Layout/IndentationConsistency:
|
59
|
+
Enabled: false
|
60
|
+
EnforcedStyle: indented_internal_methods
|
61
|
+
|
62
|
+
# Detect hard tabs, no hard tabs.
|
63
|
+
Layout/IndentationStyle:
|
64
|
+
Enabled: true
|
65
|
+
|
66
|
+
# Two spaces, no tabs (for indentation).
|
67
|
+
#
|
68
|
+
# Doesn't behave properly with private-only concerns, so it's disabled.
|
69
|
+
Layout/IndentationWidth:
|
70
|
+
Enabled: false
|
71
|
+
|
72
|
+
Layout/LeadingCommentSpace:
|
73
|
+
Enabled: true
|
74
|
+
|
75
|
+
Layout/SpaceAfterColon:
|
76
|
+
Enabled: true
|
77
|
+
|
78
|
+
Layout/SpaceAfterComma:
|
79
|
+
Enabled: true
|
80
|
+
|
81
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
82
|
+
Enabled: true
|
83
|
+
|
84
|
+
Layout/SpaceAroundKeyword:
|
85
|
+
Enabled: true
|
86
|
+
|
87
|
+
# Use `foo {}` not `foo{}`.
|
88
|
+
Layout/SpaceBeforeBlockBraces:
|
89
|
+
Enabled: true
|
90
|
+
|
91
|
+
Layout/SpaceBeforeComma:
|
92
|
+
Enabled: true
|
93
|
+
|
94
|
+
Layout/SpaceBeforeFirstArg:
|
95
|
+
Enabled: true
|
96
|
+
|
97
|
+
# Use `->(x, y) { x + y }` not `-> (x, y) { x + y }`
|
98
|
+
Layout/SpaceInLambdaLiteral:
|
99
|
+
Enabled: true
|
100
|
+
|
101
|
+
# Use `[ a, [ b, c ] ]` not `[a, [b, c]]`
|
102
|
+
# Use `[]` not `[ ]`
|
103
|
+
Layout/SpaceInsideArrayLiteralBrackets:
|
104
|
+
Enabled: true
|
105
|
+
EnforcedStyle: space
|
106
|
+
EnforcedStyleForEmptyBrackets: no_space
|
107
|
+
|
108
|
+
# Use `%w[ a b ]` not `%w[ a b ]`.
|
109
|
+
Layout/SpaceInsideArrayPercentLiteral:
|
110
|
+
Enabled: true
|
111
|
+
|
112
|
+
# Use `foo { bar }` not `foo {bar}`.
|
113
|
+
# Use `foo { }` not `foo {}`.
|
114
|
+
Layout/SpaceInsideBlockBraces:
|
115
|
+
Enabled: true
|
116
|
+
EnforcedStyleForEmptyBraces: space
|
117
|
+
|
118
|
+
# Use `{ a: 1 }` not `{a:1}`.
|
119
|
+
# Use `{}` not `{ }`.
|
120
|
+
Layout/SpaceInsideHashLiteralBraces:
|
121
|
+
Enabled: true
|
122
|
+
EnforcedStyle: space
|
123
|
+
EnforcedStyleForEmptyBraces: no_space
|
124
|
+
|
125
|
+
# Use `foo(bar)` not `foo( bar )`
|
126
|
+
Layout/SpaceInsideParens:
|
127
|
+
Enabled: true
|
128
|
+
|
129
|
+
# Requiring a space is not yet supported as of 0.59.2
|
130
|
+
# Use `%w[ foo ]` not `%w[foo]`
|
131
|
+
Layout/SpaceInsidePercentLiteralDelimiters:
|
132
|
+
Enabled: false
|
133
|
+
#EnforcedStyle: space
|
134
|
+
|
135
|
+
# Use `hash[:key]` not `hash[ :key ]`
|
136
|
+
Layout/SpaceInsideReferenceBrackets:
|
137
|
+
Enabled: true
|
138
|
+
|
139
|
+
# Blank lines should not have any spaces.
|
140
|
+
Layout/TrailingEmptyLines:
|
141
|
+
Enabled: true
|
142
|
+
|
143
|
+
# No trailing whitespace.
|
144
|
+
Layout/TrailingWhitespace:
|
145
|
+
Enabled: true
|
146
|
+
|
147
|
+
Lint/RedundantStringCoercion:
|
148
|
+
Enabled: true
|
149
|
+
|
150
|
+
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
|
151
|
+
Lint/RequireParentheses:
|
152
|
+
Enabled: true
|
153
|
+
|
154
|
+
Lint/UriEscapeUnescape:
|
155
|
+
Enabled: true
|
156
|
+
|
157
|
+
Performance:
|
158
|
+
Exclude:
|
159
|
+
- "test/**/*"
|
160
|
+
|
161
|
+
Performance/FlatMap:
|
162
|
+
Enabled: true
|
163
|
+
|
164
|
+
Performance/UnfreezeString:
|
165
|
+
Enabled: true
|
166
|
+
|
167
|
+
# Prefer assert_not over assert !
|
168
|
+
Rails/AssertNot:
|
169
|
+
Include:
|
170
|
+
- "test/**/*"
|
171
|
+
|
172
|
+
# Prefer assert_not_x over refute_x
|
173
|
+
Rails/RefuteMethods:
|
174
|
+
Include:
|
175
|
+
- "test/**/*"
|
176
|
+
|
177
|
+
# We generally prefer &&/|| but like low-precedence and/or in context
|
178
|
+
Style/AndOr:
|
179
|
+
Enabled: false
|
180
|
+
|
181
|
+
# Prefer Foo.method over Foo::method
|
182
|
+
Style/ColonMethodCall:
|
183
|
+
Enabled: true
|
184
|
+
|
185
|
+
Style/DefWithParentheses:
|
186
|
+
Enabled: true
|
187
|
+
|
188
|
+
# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
|
189
|
+
Style/HashSyntax:
|
190
|
+
Enabled: true
|
191
|
+
EnforcedShorthandSyntax: either
|
192
|
+
|
193
|
+
# Defining a method with parameters needs parentheses.
|
194
|
+
Style/MethodDefParentheses:
|
195
|
+
Enabled: true
|
196
|
+
|
197
|
+
Style/ParenthesesAroundCondition:
|
198
|
+
Enabled: true
|
199
|
+
|
200
|
+
Style/PercentLiteralDelimiters:
|
201
|
+
Enabled: true
|
202
|
+
PreferredDelimiters:
|
203
|
+
default: "()"
|
204
|
+
"%i": "[]"
|
205
|
+
"%I": "[]"
|
206
|
+
"%r": "{}"
|
207
|
+
"%w": "[]"
|
208
|
+
"%W": "[]"
|
209
|
+
|
210
|
+
# Use quotes for string literals when they are enough.
|
211
|
+
Style/RedundantPercentQ:
|
212
|
+
Enabled: false
|
213
|
+
|
214
|
+
Style/RedundantReturn:
|
215
|
+
Enabled: true
|
216
|
+
AllowMultipleReturnValues: true
|
217
|
+
|
218
|
+
Style/Semicolon:
|
219
|
+
Enabled: true
|
220
|
+
AllowAsExpressionSeparator: true
|
221
|
+
|
222
|
+
Style/StabbyLambdaParentheses:
|
223
|
+
Enabled: true
|
224
|
+
|
225
|
+
# Use `"foo"` not `'foo'` unless escaping is required
|
226
|
+
Style/StringLiterals:
|
227
|
+
Enabled: true
|
228
|
+
EnforcedStyle: double_quotes
|
229
|
+
Include:
|
230
|
+
- "app/**/*"
|
231
|
+
- "config/**/*"
|
232
|
+
- "lib/**/*"
|
233
|
+
- "test/**/*"
|
234
|
+
- "Gemfile"
|
235
|
+
|
236
|
+
Style/TrailingCommaInArrayLiteral:
|
237
|
+
Enabled: true
|
238
|
+
|
239
|
+
Style/TrailingCommaInHashLiteral:
|
240
|
+
Enabled: true
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: i18n_uno
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Haris Krajina
|
8
|
+
- hkraji
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2024-04-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '10.0'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '10.0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '3.0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '3.0'
|
42
|
+
description: i18n Uno is simple CLI tool that will completly translate your application
|
43
|
+
to any desired language.
|
44
|
+
email:
|
45
|
+
- haris@pythagoranorth.com
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- Gemfile
|
51
|
+
- Gemfile.lock
|
52
|
+
- LICENSE
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- i18n_uno.gemspec
|
56
|
+
- lib/.DS_Store
|
57
|
+
- lib/i18n_uno.rb
|
58
|
+
- lib/i18n_uno/.DS_Store
|
59
|
+
- lib/i18n_uno/arbiter.rb
|
60
|
+
- lib/i18n_uno/comparer.rb
|
61
|
+
- lib/i18n_uno/configuration.rb
|
62
|
+
- lib/i18n_uno/delta.rb
|
63
|
+
- lib/i18n_uno/file.rb
|
64
|
+
- lib/i18n_uno/files/content_change.rb
|
65
|
+
- lib/i18n_uno/open_ai_client.rb
|
66
|
+
- lib/i18n_uno/railtie.rb
|
67
|
+
- lib/i18n_uno/translator.rb
|
68
|
+
- lib/i18n_uno/tree.rb
|
69
|
+
- lib/i18n_uno/version.rb
|
70
|
+
- lib/tasks/i18n_uno.rake
|
71
|
+
- rubocop.yml
|
72
|
+
homepage: https://github.com/pythagora-north/i18n_uno
|
73
|
+
licenses:
|
74
|
+
- MIT
|
75
|
+
metadata:
|
76
|
+
homepage_uri: https://github.com/pythagora-north/i18n_uno
|
77
|
+
source_code_uri: https://github.com/pythagora-north/i18n_uno
|
78
|
+
changelog_uri: https://github.com/pythagora-north/i18n_uno/blob/main/Changes.md
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.5.3
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: i18n Uno levrages power of ChatGPT API to translate your i18n files.
|
98
|
+
test_files: []
|