extract_i18n 0.1.0

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