neeto-translate-cli 0.1.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7f02f39e18528cfe0511c15be9a69c5f5b156d0ebb7497a6f68c3fc702d70be8
4
+ data.tar.gz: 306076cb3c777e33171135b221f745175ffcd380be3c94e2daabe6dcd334672f
5
+ SHA512:
6
+ metadata.gz: b28d58ccfd9766d8d7f7660cb3e191856bcb5dcbf6d1f38a025cf925746722802d02c84319cbed03930bba33d35fc55633ca2ebb413daf67120f4eb17fc8bdb8
7
+ data.tar.gz: 282c29211bb41d7f32c48aec95491f7a7a22373bf90d4866144d3b950f94eb170461a4b85dce698c4b4c6fa266ea01e5048d7ef2c3c6b729c021e28c7756d6a7
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # neeto-translate-cli
2
+
3
+ A Ruby CLI tool that extracts translation files from Rails applications and makes requests to the neeto-translate API for automated translation management.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Usage](#usage)
9
+ - [Development](#development)
10
+
11
+ ## Installation
12
+
13
+ Install the gem via RubyGems:
14
+
15
+ ```bash
16
+ gem install neeto-translate-cli
17
+ ```
18
+
19
+ Or add it to your Gemfile:
20
+
21
+ ```ruby
22
+ gem 'neeto-translate-cli'
23
+ ```
24
+
25
+ Then run:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ The `neeto-translate-cli` tool helps you manage translations by:
34
+
35
+ 1. Extracting translation keys from your Rails application's frontend (JSON) and backend (YAML) locale files
36
+ 2. Identifying missing translations across different languages
37
+ 3. Detecting updated translation keys since the last automated translation
38
+ 4. Sending translation requests to the neeto-translate API
39
+
40
+ ### Basic Usage
41
+
42
+ ```bash
43
+ neeto-translate-cli [OPTIONS]
44
+ ```
45
+
46
+ #### Command line options
47
+
48
+ **Options:**
49
+
50
+ - `--frontend PATH` - Path to frontend locales (default: `app/javascript/src/translations`)
51
+ - `--backend PATH` - Path to backend locales (default: `config/locales`)
52
+ - `--defaults LANGUAGES` - Comma-separated list of languages to translate (default: `en,fr,de,es`)
53
+ - `--repo NAME` - GitHub repository name (e.g., `neetozone/neeto-cal-web`)
54
+
55
+ ### Examples
56
+
57
+ **Custom paths:**
58
+
59
+ ```bash
60
+ neeto-translate-cli --frontend src/locales --backend config/translations
61
+ ```
62
+
63
+ **Specific languages:**
64
+
65
+ ```bash
66
+ neeto-translate-cli --defaults "fr,de,es,it"
67
+ ```
68
+
69
+ **Full example:**
70
+
71
+ ```bash
72
+ neeto-translate-cli \
73
+ --frontend app/javascript/src/translations \
74
+ --backend config/locales \
75
+ --defaults "fr,de,es" \
76
+ --repo "neetozone/neeto-cal-web"
77
+ ```
78
+
79
+ ### Environment Variables
80
+
81
+ The following environment variables can be configured:
82
+
83
+ - `NEETO_TRANSLATE_URL` - Base URL for the neeto-translate API (default: `https://translate.neeto.com`)
84
+ - `NEETO_TRANSLATE_X_TOKEN` - Authentication token for the API
85
+ - `DEFAULT_LANGUAGES` - Comma-separated list of default languages (default: `en,fr,de,es`)
86
+
87
+ ### How It Works
88
+
89
+ 1. **Extraction**: The tool reads your English translation files (`en.json` for frontend, `en.yml` for backend)
90
+ 2. **Analysis**: It compares with other language files to identify missing translations
91
+ 3. **Change Detection**: It detects keys that have been updated since the last automated translation run
92
+ 4. **API Request**: Sends a payload to the neeto-translate API with:
93
+ - Current English translations
94
+ - Missing translation keys for each language
95
+ - Updated keys that need re-translation
96
+ - Repository metadata
97
+
98
+ ## Development
99
+
100
+ ### Setup
101
+
102
+ 1. Clone the repository:
103
+
104
+ ```bash
105
+ git clone https://github.com/neetozone/neeto-translate.git
106
+ cd neeto-translate/cli
107
+ ```
108
+
109
+ 2. Install dependencies:
110
+
111
+ ```bash
112
+ bundle install
113
+ ```
114
+
115
+ 3. Run tests:
116
+
117
+ ```bash
118
+ bundle exec rake test
119
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require_relative "../lib/neeto_translate_cli/cli"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -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,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/neeto_translate_cli/cli"
4
+
5
+ NeetoTranslateCli::CLI.start(ARGV)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module NeetoTranslateCli
7
+ class Api
8
+ def translate!(payload)
9
+ connection.post("/translation") do |request|
10
+ request.body = payload.to_json
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def connection
17
+ @connection ||= Faraday.new(url: url, headers: headers)
18
+ end
19
+
20
+ def url
21
+ ENV["NEETO_TRANSLATE_URL"] || CLI::NEETO_TRANSLATE_URL
22
+ end
23
+
24
+ def headers
25
+ {
26
+ "Content-Type" => "application/json",
27
+ "NEETO-TRANSLATE-X-TOKEN" => ENV["NEETO_TRANSLATE_X_TOKEN"]
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "translator"
4
+
5
+ require "optparse"
6
+
7
+ module NeetoTranslateCli
8
+ class CLI
9
+ DEFAULT_LANGUAGES = "en,fr,de,es"
10
+ NEETO_TRANSLATE_URL = "https://translate.neeto.com"
11
+ DEFAULT_BACKEND_LOCALE_PATH = "config/locales"
12
+ DEFAULT_FRONTEND_LOCALE_PATH = "app/javascript/src/translations"
13
+
14
+ def self.start(args)
15
+ new(args).run
16
+ end
17
+
18
+ def initialize(args)
19
+ @args = args
20
+ end
21
+
22
+ def run
23
+ options = {
24
+ frontend: DEFAULT_FRONTEND_LOCALE_PATH,
25
+ backend: DEFAULT_BACKEND_LOCALE_PATH,
26
+ default_languages: (ENV["DEFAULT_LANGUAGES"] || DEFAULT_LANGUAGES).to_s.split(","),
27
+ repo: `git rev-parse --show-toplevel`.split("/").last.strip
28
+ }
29
+
30
+ OptionParser.new do |opts|
31
+ opts.on("--frontend PATH", "Path to frontend locales.") { |path| options[:frontend] = path }
32
+ opts.on("--backend PATH", "Path to backend locales") { |path| options[:backend] = path }
33
+ opts.on("--defaults ARRAY", "An array of comma separated languages to be translated") do |languages|
34
+ options[:default_languages] = languages.to_s.split(",")
35
+ end
36
+ opts.on("--repo NAME", "Name of the GitHub repository (eg: neetozone/neeto-cal-web).") do |path|
37
+ options[:repo] = path
38
+ end
39
+ end.parse!(@args)
40
+
41
+ Translator.new(options).process!
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,119 @@
1
+ require "json"
2
+ require "yaml"
3
+
4
+ module NeetoTranslateCli
5
+ class PayloadBuilder
6
+ COMMIT_AUTHOR = "neetobot"
7
+
8
+ attr_reader :options, :updated_keys
9
+
10
+ def initialize(options)
11
+ @options = options
12
+ commit_id = `git log --author="#{COMMIT_AUTHOR}" --grep="\[neeto-translate\]" -n 1 --pretty=format:"%H"`.strip
13
+ @updated_keys = commit_id.empty? ? { frontend: [], backend: [] } : find_updated_keys(commit_id)
14
+ end
15
+
16
+ def process!
17
+ {
18
+ frontend: { en: en_json_flattened, missing_keys: find_missing_keys_for(:frontend) },
19
+ backend: { en: en_yml_flattened, missing_keys: find_missing_keys_for(:backend) },
20
+ metadata: {
21
+ repo: options[:repo],
22
+ frontend: options[:frontend],
23
+ backend: options[:backend]
24
+ }
25
+ }
26
+ end
27
+
28
+ def no_new_changes?(payload)
29
+ payload[:frontend][:missing_keys].values.all?(&:empty?) && payload[:backend][:missing_keys].values.all?(&:empty?)
30
+ end
31
+
32
+ private
33
+
34
+ def en_json_flattened
35
+ @_en_json_flattened ||= flatten_hash(load_json(File.expand_path("en.json", options[:frontend])))
36
+ end
37
+
38
+ def en_yml_flattened
39
+ @_en_yml_flattened ||= flatten_hash(load_yml(File.expand_path("en.yml", options[:backend]))["en"])
40
+ end
41
+
42
+ def find_updated_keys(commit_id)
43
+ previous_en_json_content = `git show #{commit_id}:#{options[:frontend]}/en.json`
44
+ updated_frontend_keys = []
45
+ unless previous_en_json_content.empty?
46
+ previous_en_json = JSON.parse(previous_en_json_content)
47
+ flat_previous_en_json = flatten_hash(previous_en_json)
48
+ flat_current_en_json = en_json_flattened
49
+
50
+ updated_frontend_keys = flat_current_en_json.keys.select do |key|
51
+ flat_previous_en_json.key?(key) && flat_current_en_json[key] != flat_previous_en_json[key]
52
+ end
53
+ end
54
+
55
+ previous_en_yml_content = `git show #{commit_id}:#{options[:backend]}/en.yml`
56
+ updated_backend_keys = []
57
+ unless previous_en_yml_content.empty?
58
+ previous_en_yml = YAML.load(previous_en_yml_content, aliases: true)["en"]
59
+ flat_previous_en_yml = flatten_hash(previous_en_yml)
60
+ flat_current_en_yml = en_yml_flattened
61
+
62
+ updated_backend_keys = flat_current_en_yml.keys.select do |key|
63
+ flat_previous_en_yml.key?(key) && flat_current_en_yml[key] != flat_previous_en_yml[key]
64
+ end
65
+ end
66
+
67
+ { frontend: updated_frontend_keys, backend: updated_backend_keys }
68
+ end
69
+
70
+ def find_missing_keys_for(type)
71
+ dir = options[type]
72
+ if type == :frontend
73
+ ext = "json"
74
+ base_keys = en_json_flattened.keys
75
+ else
76
+ ext = "yml"
77
+ base_keys = en_yml_flattened.keys
78
+ end
79
+
80
+ languages.each_with_object({}) do |lang, missing|
81
+ file_path = File.expand_path("#{lang}.#{ext}", dir)
82
+ if File.file?(file_path)
83
+ translations = ext == "json" ? load_json(file_path) : load_yml(file_path)[lang]
84
+ mk = base_keys - flatten_hash(translations).keys
85
+ else
86
+ mk = base_keys
87
+ end
88
+
89
+ missing[lang] = (mk + updated_keys[type]).uniq
90
+ end
91
+ end
92
+
93
+ def load_json(file_path)
94
+ JSON.parse(File.read(file_path, encoding: "utf-8"))
95
+ end
96
+
97
+ def load_yml(file_path)
98
+ YAML.load_file(file_path, aliases: true)
99
+ end
100
+
101
+ def flatten_hash(hash, prefix = nil)
102
+ hash.each_with_object({}) do |(key, value), result|
103
+ current_key = prefix ? "#{prefix}.#{key}" : key.to_s
104
+ if value.is_a?(Hash)
105
+ result.merge!(flatten_hash(value, current_key))
106
+ else
107
+ result[current_key] = value
108
+ end
109
+ end
110
+ end
111
+
112
+ def languages
113
+ default_languages = options[:default_languages]
114
+ project_languages = Dir.glob(File.join(options[:frontend], "*.json")).map { |f| File.basename(f, ".json") }
115
+
116
+ (default_languages + project_languages).uniq - ["en"]
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "api"
4
+ require_relative "payload_builder"
5
+
6
+ module NeetoTranslateCli
7
+ class Translator
8
+ attr_reader :api, :options, :job_id
9
+
10
+ def initialize(options)
11
+ @api = NeetoTranslateCli::Api.new
12
+ @options = options
13
+ end
14
+
15
+ def process!
16
+ translate!
17
+ end
18
+
19
+ private
20
+
21
+ def translate!
22
+ payload_builder = PayloadBuilder.new(options)
23
+ payload = payload_builder.process!
24
+
25
+ if payload_builder.no_new_changes?(payload)
26
+ puts "Nothing to see here. You are all good."
27
+ else
28
+ response = api.translate! payload
29
+ puts response.body
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoTranslateCli
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,23 @@
1
+ en:
2
+ common:
3
+ success: "Success"
4
+ error: "An error occurred"
5
+ not_found: "Not Found"
6
+ activerecord:
7
+ models:
8
+ user: "User"
9
+ product: "Product"
10
+ attributes:
11
+ user:
12
+ name: "Name"
13
+ email: "Email"
14
+ product:
15
+ name: "Product Name"
16
+ price: "Price"
17
+ mailer:
18
+ welcome:
19
+ subject: "Welcome to our service!"
20
+ body: "Hi %{name}, welcome!"
21
+ password_reset:
22
+ subject: "Password Reset Request"
23
+ body: "Please click the link to reset your password."
@@ -0,0 +1,15 @@
1
+ es:
2
+ common:
3
+ success: "Éxito"
4
+ error: "Ocurrió un error"
5
+ activerecord:
6
+ models:
7
+ user: "Usuario"
8
+ product: "Producto"
9
+ attributes:
10
+ product:
11
+ name: "Nombre del Producto"
12
+ mailer:
13
+ welcome:
14
+ subject: "¡Bienvenido a nuestro servicio!"
15
+ body: "Hola %{name}, ¡bienvenido!"
@@ -0,0 +1,18 @@
1
+ jp:
2
+ common:
3
+ success: "成功"
4
+ not_found: "見つかりません"
5
+ activerecord:
6
+ models:
7
+ user: "ユーザー"
8
+ attributes:
9
+ user:
10
+ name: "名前"
11
+ email: "メールアドレス"
12
+ product:
13
+ name: "製品名"
14
+ price: "価格"
15
+ mailer:
16
+ password_reset:
17
+ subject: "パスワードリセットリクエスト"
18
+ body: "リンクをクリックしてパスワードをリセットしてください。"
@@ -0,0 +1,22 @@
1
+ {
2
+ "common": {
3
+ "ok": "OK",
4
+ "cancel": "Cancel",
5
+ "submit": "Submit"
6
+ },
7
+ "login": {
8
+ "title": "Login",
9
+ "email": "Email Address",
10
+ "password": "Password",
11
+ "forgot_password": "Forgot your password?"
12
+ },
13
+ "dashboard": {
14
+ "title": "Dashboard",
15
+ "welcome_message": "Welcome, {{name,anyCase}}",
16
+ "stats": {
17
+ "users": "Users",
18
+ "revenue": "Revenue",
19
+ "orders": "Orders"
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "common": {
3
+ "ok": "Aceptar",
4
+ "submit": "Enviar"
5
+ },
6
+ "login": {
7
+ "title": "Iniciar Sesión"
8
+ },
9
+ "dashboard": {
10
+ "title": "Tablero",
11
+ "welcome_message": "¡Bienvenido, {{name,anyCase}}"
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "common": {
3
+ "ok": "はい",
4
+ "cancel": "キャンセル"
5
+ },
6
+ "login": {
7
+ "title": "ログイン",
8
+ "email": "メールアドレス",
9
+ "password": "パスワード",
10
+ "forgot_password": "パスワードをお忘れですか?"
11
+ },
12
+ "dashboard": {
13
+ "title": "ダッシュボード",
14
+ "stats": {
15
+ "users": "ユーザー",
16
+ "orders": "注文"
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,63 @@
1
+ require "test_helper"
2
+
3
+ class NeetoTranslateCli::PayloadBuilderTest < Minitest::Test
4
+ def setup
5
+ @options = {
6
+ frontend: "test/fixtures/frontend",
7
+ backend: "test/fixtures/backend",
8
+ default_languages: %w[es de]
9
+ }
10
+ @payload_builder = NeetoTranslateCli::PayloadBuilder.new(@options)
11
+ @payload = @payload_builder.process!
12
+ end
13
+
14
+ def test_correct_frontend_payload_is_constructured
15
+ en_json_path = File.expand_path("en.json", @options[:frontend])
16
+ en_json = JSON.parse(File.read(en_json_path, encoding: "utf-8"))
17
+ es_missing_keys = [
18
+ "common.cancel",
19
+ "dashboard.stats.orders",
20
+ "dashboard.stats.revenue",
21
+ "dashboard.stats.users",
22
+ "login.email",
23
+ "login.forgot_password",
24
+ "login.password"
25
+ ]
26
+
27
+ assert @payload[:frontend]
28
+ assert_equal flatten_hash(en_json), @payload[:frontend][:en]
29
+ assert_equal %w[es de jp], @payload_builder.send(:languages)
30
+ assert_equal es_missing_keys, @payload.dig(:frontend, :missing_keys, "es").sort
31
+ end
32
+
33
+ def test_correct_backend_payload_is_constructured
34
+ en_yml_path = File.expand_path("en.yml", @options[:backend])
35
+ en_yml = YAML.load_file(en_yml_path, aliases: true)["en"]
36
+ es_missing_keys = [
37
+ "activerecord.attributes.product.price",
38
+ "activerecord.attributes.user.email",
39
+ "activerecord.attributes.user.name",
40
+ "common.not_found",
41
+ "mailer.password_reset.body",
42
+ "mailer.password_reset.subject"
43
+ ]
44
+
45
+ assert @payload[:backend]
46
+ assert_equal flatten_hash(en_yml), @payload[:backend][:en]
47
+ assert_equal %w[es de jp], @payload_builder.send(:languages)
48
+ assert_equal es_missing_keys, @payload.dig(:backend, :missing_keys, "es").sort
49
+ end
50
+
51
+ private
52
+
53
+ def flatten_hash(hash, prefix = nil)
54
+ hash.each_with_object({}) do |(key, value), result|
55
+ current_key = prefix ? "#{prefix}.#{key}" : key.to_s
56
+ if value.is_a?(Hash)
57
+ result.merge!(flatten_hash(value, current_key))
58
+ else
59
+ result[current_key] = value
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/neeto_translate_cli/cli"
4
+
5
+ require "minitest/autorun"
6
+ require "pry-byebug"
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: neeto-translate-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Abhay V Ashokan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-07-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: neeto-translate CLI tool to extract translation files and make requests
28
+ to the neeto-translate API
29
+ email:
30
+ - abhay.ashokan@bigbinary.com
31
+ executables:
32
+ - neeto-translate-cli
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - README.md
37
+ - Rakefile
38
+ - bin/console
39
+ - bin/setup
40
+ - exe/neeto-translate-cli
41
+ - lib/neeto_translate_cli/api.rb
42
+ - lib/neeto_translate_cli/cli.rb
43
+ - lib/neeto_translate_cli/payload_builder.rb
44
+ - lib/neeto_translate_cli/translator.rb
45
+ - lib/neeto_translate_cli/version.rb
46
+ - test/fixtures/backend/en.yml
47
+ - test/fixtures/backend/es.yml
48
+ - test/fixtures/backend/jp.yml
49
+ - test/fixtures/frontend/en.json
50
+ - test/fixtures/frontend/es.json
51
+ - test/fixtures/frontend/jp.json
52
+ - test/neeto_translate_cli/payload_builder_test.rb
53
+ - test/test_helper.rb
54
+ homepage: https://github.com/neetozone/neeto-translate
55
+ licenses: []
56
+ metadata:
57
+ homepage_uri: https://github.com/neetozone/neeto-translate
58
+ source_code_uri: https://github.com/neetozone/neeto-translate
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.1.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.5.16
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: neeto-translate CLI tool to extract translation files and make requests to
78
+ the neeto-translate API
79
+ test_files: []