extract_i18n 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e3434dc96f1e175bb499bfcdf6d4bf1b5dcc5a1a76190639c8689c0eee069820
4
+ data.tar.gz: a6bd0093f55639253d1f9dd5589f9ebdefda5197e4ccb04d9b01dd1ce2fade88
5
+ SHA512:
6
+ metadata.gz: 82b5167ce206cc40b507d9e7eafae30f8b04384670483ed328c8a924498c252581a87bf02cf03fa103e7591c13090b6a8c97046028af974c18f15489e328b23a
7
+ data.tar.gz: 0e94e6d16ffe2e67d18be3d5a06d82051c3a713fdd03e0be8983b4f4a5f2097777f68935437605ef0cd788025c8dd5d4a0fbeb9c209c3d8074b07cf7915211c6
@@ -0,0 +1,23 @@
1
+ name: Verify
2
+ on: [push]
3
+
4
+ jobs:
5
+ tests:
6
+ name: Tests
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ # ruby: [ '2.5', '2.6', '2.7' ]
11
+ ruby: [ '2.6' ]
12
+ steps:
13
+ - uses: actions/checkout@v2
14
+ - uses: ruby/setup-ruby@v1
15
+ with:
16
+ ruby-version: ${{ matrix.ruby }}
17
+ - name: Install gems
18
+ run: |
19
+ bundle config path vendor/bundle
20
+ bundle install --jobs 4 --retry 3
21
+ - name: Run tests
22
+ run: bundle exec rspec
23
+
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ spec/.examples.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,35 @@
1
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
2
+ # configuration file. It makes it possible to enable/disable
3
+ # certain cops (checks) and to alter their behavior if they accept
4
+ # any parameters. The file can be placed either in your home
5
+ # directory or in some project directory.
6
+ #
7
+ # RuboCop will start looking for the configuration file in the directory
8
+ # where the inspected file is and continue its way up to the root directory.
9
+ #
10
+ # See https://docs.rubocop.org/rubocop/configuration
11
+
12
+
13
+ Style:
14
+ Enabled: false
15
+ Metrics:
16
+ Enabled: false
17
+
18
+ AllCops:
19
+ NewCops: enable
20
+ Exclude:
21
+ - "lib/extract_i18n/adapters/slim_adapter_wip.rb"
22
+
23
+ Naming/PredicateName:
24
+ Enabled: false
25
+ Layout/DotPosition:
26
+ EnforcedStyle: trailing
27
+ Layout/EmptyLineAfterGuardClause:
28
+ Enabled: false
29
+ Lint/MissingCopEnableDirective:
30
+ Enabled: false
31
+ Lint/MixedRegexpCaptureTypes:
32
+ Enabled: false
33
+ Layout/LineLength:
34
+ Exclude:
35
+ - "spec/**/*.rb"
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in extract_i18n.gemspec
8
+ gemspec
9
+
10
+ gem 'pry'
11
+ gem 'rake'
12
+ gem 'rspec'
13
+ gem 'rubocop'
14
+ gem 'solargraph'
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Stefan Wienert
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,52 @@
1
+ # ExtractI18n
2
+
3
+ CLI helper program to automatically extract bare text strings into Rails I18n interactively.
4
+
5
+ Useful when adding i18n to a medium/large Rails app.
6
+
7
+ This Gem **supports** the following source files:
8
+
9
+ - Ruby files (controllers, models etc.) via Ruby-Parser, e.g. walking all Ruby Strings
10
+ - Slim Views (via Regexp parser by SlimKeyfy)
11
+ - Vue Pug views
12
+ - Pug is very similar to slim and thus relatively good extractable via Regexp.
13
+
14
+ CURRENTLY THERE IS **NO SUPPORT** FOR:
15
+
16
+ - erb ( integrating/forking https://github.com/zigzag/ready_for_i18n or https://github.com/ProGM/i18n-html_extractor)
17
+ - haml ( integrating https://github.com/shaiguitar/haml-i18n-extractor)
18
+ - vue html templates ([Check out my vue pug converting script](https://gist.github.com/zealot128/6c41df1d33a810856a557971a04989f6))
19
+
20
+ But I am open to integrating PRs for those!
21
+
22
+ I strongly recommend using a Source-Code-Management (Git) and ``i18n-tasks`` for checking the key consistency.
23
+ I've created a scanner to make that work with vue $t structures too: https://gist.github.com/zealot128/e6ec1767a40a6c3d85d7f171f4d88293
24
+
25
+ ## Installation
26
+
27
+ install:
28
+
29
+ $ gem install extract_i18n
30
+
31
+ ## Usage
32
+
33
+ DO USE A SOURCE-CODE-MANAGEMENT-SYSTEM (Git). There is no guarantee that programm will not destroy your workspace :)
34
+
35
+
36
+ ```
37
+ extract-i18n --helper
38
+
39
+ extract-i18n --locale de --yaml config/locales/unsorted.de.yml app/views/user
40
+ ```
41
+
42
+ If you prefer relative keys in slim views use ``--slim-relative``, e.g. ``t('.title')`` instead of ``t('users.index.title')``.
43
+ I prefer absolute keys, as it makes copy pasting/ moving files much safer.
44
+
45
+
46
+ ## Contributing
47
+
48
+ Bug reports and pull requests are welcome on GitHub at https://github.com/zealot128/extract_i18n.
49
+
50
+ ## License
51
+
52
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "extract_i18n"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
4
+ require 'extract_i18n/cli'
5
+
6
+ result = ExtractI18n::CLI.new.run
7
+ exit(result ? 0 : 1)
@@ -0,0 +1,34 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "extract_i18n/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "extract_i18n"
7
+ spec.version = ExtractI18n::VERSION
8
+ spec.authors = ["Stefan Wienert"]
9
+ spec.email = ["info@stefanwienert.de"]
10
+
11
+ spec.summary = %q{Extact i18n from Ruby files using Ruby parser and slim files using regex}
12
+ spec.description = %q{Extact i18n from Ruby files using Ruby parser and slim files using regex interactively}
13
+ spec.homepage = "https://github.com/pludoni/extract_i18n"
14
+ spec.license = "MIT"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_runtime_dependency 'parser', '>= 2.6'
29
+ spec.add_runtime_dependency 'slim'
30
+ spec.add_runtime_dependency 'tty-prompt'
31
+ spec.add_runtime_dependency 'zeitwerk'
32
+ spec.add_dependency "diff-lcs"
33
+ spec.add_dependency "diffy"
34
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "extract_i18n/version"
4
+
5
+ require "zeitwerk"
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup # ready!
8
+
9
+ module ExtractI18n
10
+ class << self
11
+ attr_accessor :strip_path, :ignore_hash_keys, :ignore_functions, :ignorelist
12
+ end
13
+
14
+ self.strip_path = %r{^app/(javascript|controllers|views)|^lib|^src|^app}
15
+
16
+ # ignore for .rb files: ignore those file types
17
+ self.ignore_hash_keys = %w[class_name foreign_key join_table association_foreign_key key]
18
+ self.ignore_functions = %w[where order group select sql]
19
+ self.ignorelist = [
20
+ '_',
21
+ '::'
22
+ ]
23
+
24
+ def self.key(string, length: 25)
25
+ string.strip.
26
+ unicode_normalize(:nfkd).gsub(/(\p{Letter})\p{Mark}+/, '\\1').
27
+ gsub(/\W+/, '_').downcase[0..length].
28
+ gsub(/_+$|^_+/, '')
29
+ end
30
+
31
+ def self.file_key(path)
32
+ path.gsub(strip_path, '').
33
+ gsub(%r{^/|/$}, '').
34
+ gsub(/\.(vue|rb|html\.slim|\.slim)$/, '').
35
+ gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').
36
+ gsub('/', '.').
37
+ tr("-", "_").downcase
38
+ end
39
+ end
40
+
41
+ require 'extract_i18n/file_processor'
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExtractI18n::Adapters
4
+ class Adapter
5
+ def self.for(file_path)
6
+ case file_path
7
+ when /\.rb$/ then RubyAdapter
8
+ when /\.slim$/ then SlimAdapter
9
+ when /\.vue$/ then VueAdapter
10
+ end
11
+ end
12
+
13
+ attr_reader :on_ask, :file_path, :file_key, :options
14
+
15
+ def initialize(file_key:, on_ask:, options: {})
16
+ @on_ask = on_ask
17
+ @file_key = file_key
18
+ @options = options
19
+ end
20
+
21
+ def run(content)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def self.supports_relative_keys?
26
+ false
27
+ end
28
+
29
+ private
30
+
31
+ def original_content
32
+ @original_content ||= File.read(file_path)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+ require 'tty-prompt'
5
+ require 'pry'
6
+ require 'pastel'
7
+ require 'yaml'
8
+ require 'extract_i18n/source_change'
9
+
10
+ module ExtractI18n::Adapters
11
+ class RubyAdapter < Adapter
12
+ def run(original_content)
13
+ buffer = Parser::Source::Buffer.new('(example)')
14
+ buffer.source = original_content
15
+ temp = Parser::CurrentRuby.parse(original_content)
16
+ rewriter = ExtractI18n::Adapters::Rewriter.new(
17
+ file_key: file_key,
18
+ on_ask: on_ask
19
+ )
20
+ # Rewrite the AST, returns a String with the new form.
21
+ rewriter.rewrite(buffer, temp)
22
+ # rescue StandardError => e
23
+ # puts 'Parsing error'
24
+ # puts e.inspect
25
+ end
26
+ end
27
+
28
+ class Rewriter < Parser::TreeRewriter
29
+ PROMPT = TTY::Prompt.new
30
+ PASTEL = Pastel.new
31
+
32
+ def initialize(file_key:, on_ask:)
33
+ @file_key = file_key
34
+ @on_ask = on_ask
35
+ end
36
+
37
+ def process(node)
38
+ @nesting ||= []
39
+ @nesting.push(node)
40
+ super
41
+ @nesting.pop
42
+ end
43
+
44
+ def on_dstr(node)
45
+ if ignore?(node, parent: @nesting[-2])
46
+ return
47
+ end
48
+ interpolate_arguments = {}
49
+ out_string = ""
50
+ node.children.each do |i|
51
+ if i.type == :str
52
+ out_string += i.children.first
53
+ else
54
+ inner_source = i.children[0].loc.expression.source.gsub(/^#\{|}$/, '')
55
+ interpolate_key = ExtractI18n.key(inner_source)
56
+ out_string += "%{#{interpolate_key}}"
57
+ interpolate_arguments[interpolate_key] = inner_source
58
+ end
59
+ end
60
+
61
+ i18n_key = ExtractI18n.key(node.children.select { |i| i.type == :str }.map { |i| i.children[0] }.join(' '))
62
+
63
+ ask_and_continue(
64
+ i18n_key: i18n_key, i18n_string: out_string, interpolate_arguments: interpolate_arguments, node: node,
65
+ )
66
+ end
67
+
68
+ def on_str(node)
69
+ string = node.children.first
70
+ if ignore?(node, parent: @nesting[-2])
71
+ return
72
+ end
73
+ ask_and_continue(i18n_key: ExtractI18n.key(string), i18n_string: string, node: node)
74
+ end
75
+
76
+ private
77
+
78
+ def ask_and_continue(i18n_key:, i18n_string:, interpolate_arguments: {}, node:)
79
+ change = ExtractI18n::SourceChange.new(
80
+ i18n_key: "#{@file_key}.#{i18n_key}",
81
+ i18n_string: i18n_string,
82
+ interpolate_arguments: interpolate_arguments,
83
+ source_line: node.location.expression.source_line,
84
+ remove: node.loc.expression.source
85
+ )
86
+ if @on_ask.call(change)
87
+ replace_content(node, change.i18n_t)
88
+ end
89
+ end
90
+
91
+ def log(string)
92
+ puts string
93
+ end
94
+
95
+ def replace_content(node, content)
96
+ if node.loc.is_a?(Parser::Source::Map::Heredoc)
97
+ replace(node.loc.expression.join(node.loc.heredoc_end), content)
98
+ else
99
+ replace(node.loc.expression, content)
100
+ end
101
+ end
102
+
103
+ def ignore?(node, parent: nil)
104
+ unless node.respond_to?(:children)
105
+ return false
106
+ end
107
+ if parent && ignore_parent?(parent)
108
+ return true
109
+ end
110
+ if node.type == :str
111
+ ExtractI18n.ignorelist.any? { |item| node.children[0][item] }
112
+ else
113
+ node.children.any? { |child|
114
+ ignore?(child)
115
+ }
116
+ end
117
+ end
118
+
119
+ def ignore_parent?(node)
120
+ node.children[1] == :require ||
121
+ node.type == :regexp ||
122
+ (node.type == :pair && ExtractI18n.ignore_hash_keys.include?(node.children[0].children[0].to_s)) ||
123
+ (node.type == :send && ExtractI18n.ignore_functions.include?(node.children[1].to_s))
124
+ end
125
+ end
126
+ end