file_tree_visualizer 0.0.3 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9ec7a6ef5ea0597b5efc056da74912985e0ca2f9a784398c13aa83b5e95c5a6
4
- data.tar.gz: f3a9a8ee8fb984e8b135fefbe9db34b9e8f5df19ccf787538a754b975b5c9022
3
+ metadata.gz: 487ea44c103965a0bf633fc0d618c50fb4a0c7d8ddbbee53b61f4d99a959d3f9
4
+ data.tar.gz: 89a961a923ab5ae1a9f83ada18f1951905e5ccde53219de8ba83fe5bd34b14f1
5
5
  SHA512:
6
- metadata.gz: f902440430610f789db16ab282b2890e3569fa354691ee74dd1587539ad299aa2b7db9c66890496fd46429e1369da7b5d84db95b6c1614977559ffbef3b99c68
7
- data.tar.gz: 9571ce07d3162b7e5477ae40f1a844c61e2ac476780e1665300778e1d811500b1b288e064b376e86a2f2305e1fad3e54a3a2dbbd80c106459f8638d4d96b835c
6
+ metadata.gz: fd1eddbd5b1ae2b077e521f8ddd09dad953c12679aac80da81253cb18673a2f300d5146f86e244da85888b4872e59e823174c7a35e47f58aaa76cfa859e23cea
7
+ data.tar.gz: 7e91afaea1510c5c3ea03695666a1edac1390bd1be55c15c5bd9a6092bdc10a35f981bce7289f0a9a1e05ef5f0823c84a4da54161929a42868150dda92d90729
data/Gemfile CHANGED
@@ -3,3 +3,5 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
+ gem "rake", "~> 13.0"
7
+ gem "minitest", "~> 5.20"
data/README.md CHANGED
@@ -1,44 +1,75 @@
1
1
  # file_tree_visualizer
2
2
 
3
- Gema Ruby para escanear árboles de archivos desde terminal y generar un reporte HTML interactivo 100% offline con los elementos que más espacio ocupan.
3
+ Ruby gem to scan file trees from the terminal and generate a fully offline interactive HTML report with the items that consume the most disk space.
4
4
 
5
- ## Qué hace
5
+ ## What it does
6
6
 
7
- - Recorre recursivamente una carpeta.
8
- - Calcula tamaño acumulado por directorio y tamaño por archivo.
9
- - Genera un reporte HTML con:
10
- - Árbol de archivos expandible/colapsable.
11
- - Filtros por texto y tamaño mínimo.
12
- - Ranking de archivos más pesados.
13
- - Sin dependencias externas (funciona sin internet).
7
+ - Recursively scans a directory.
8
+ - Computes cumulative size per directory and size per file.
9
+ - Generates an HTML report with:
10
+ - Expand/collapse file tree.
11
+ - Text and minimum-size filters.
12
+ - Ranking of the largest files.
13
+ - No external dependencies (works offline).
14
14
 
15
- ## Uso rápido (sin instalar la gema)
15
+ ## Quick usage (without installing the gem)
16
16
 
17
17
  ```bash
18
- ruby bin/file-tree-visualizer /ruta/a/escanear -o reporte.html
18
+ ruby bin/file-tree-visualizer /path/to/scan -o report.html
19
19
  ```
20
20
 
21
- ## Uso como gema
21
+ ## Install from RubyGems
22
+
23
+ Gem page: https://rubygems.org/gems/file_tree_visualizer
24
+
25
+ ```bash
26
+ gem install file_tree_visualizer
27
+ file-tree-visualizer /path/to/scan -o report.html
28
+ ```
29
+
30
+ Install a specific version:
31
+
32
+ ```bash
33
+ gem install file_tree_visualizer -v 0.1.0
34
+ ```
35
+
36
+ ## Local development install
22
37
 
23
38
  ```bash
24
39
  bundle install
25
40
  gem build file_tree_visualizer.gemspec
26
- gem install ./file_tree_visualizer-0.0.3.gem
27
- file-tree-visualizer /ruta/a/escanear -o reporte.html
41
+ gem install ./file_tree_visualizer-0.1.0.gem
42
+ file-tree-visualizer /path/to/scan -o report.html
28
43
  ```
29
44
 
30
- ## Opciones
45
+ ## Options
31
46
 
32
47
  ```bash
33
- file-tree-visualizer [ruta] [opciones]
48
+ file-tree-visualizer [path] [options]
49
+
50
+ If no `path` is provided, the current directory is scanned by default.
34
51
 
35
- Si no indicas `ruta`, se escanea el directorio actual por defecto.
52
+ During scanning, an interactive terminal shows a progress bar with processed files/directories vs discovered totals.
36
53
 
37
- Durante el escaneo, en terminal interactiva se muestra una barra de progreso con archivos y directorios procesados vs total detectado.
54
+ -o, --output FILE Output HTML file (default: file-tree-report.html)
55
+ -t, --top N Number of largest files to keep in ranking (default: 200)
56
+ --follow-symlinks Follow symbolic links
57
+ --no-progress Disable progress bar
58
+ --lang LANG Language for CLI and report (en|es)
59
+ -h, --help Show help
60
+
61
+ Language resolution priority:
62
+ - `--lang`
63
+ - `LANG` environment variable (for example `es_CL.UTF-8`)
64
+ - fallback to English (`en`)
65
+ ```
38
66
 
39
- -o, --output FILE Archivo HTML de salida (default: file-tree-report.html)
40
- -t, --top N Cantidad de archivos pesados a guardar en ranking (default: 200)
41
- --follow-symlinks Sigue enlaces simbólicos
42
- --no-progress Desactiva la barra de progreso
43
- -h, --help Muestra ayuda
67
+ ## Automatic release to RubyGems
68
+
69
+ - This repository publishes the gem automatically when pushing a tag with format `v*` (for example `v0.0.4`).
70
+ - Before using it, configure the `RUBYGEMS_API_KEY` secret in GitHub Actions.
71
+
72
+ ```bash
73
+ git tag v0.0.4
74
+ git push origin v0.0.4
44
75
  ```
data/Rakefile CHANGED
@@ -1,3 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.pattern = "test/**/*_test.rb"
9
+ end
10
+
11
+ task default: :test
@@ -1,33 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/file_tree_visualizer/version"
3
+ require_relative 'lib/file_tree_visualizer/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "file_tree_visualizer"
6
+ spec.name = 'file_tree_visualizer'
7
7
  spec.version = FileTreeVisualizer::VERSION
8
- spec.authors = ["jbidwell1"]
9
- spec.email = ["devnull@example.com"]
8
+ spec.authors = ['jbidwell1']
9
+ spec.email = ['devnull@example.com']
10
10
 
11
- spec.summary = "CLI Ruby tool to analyze file sizes and generate an interactive HTML report."
12
- spec.description = "Scans a directory recursively, computes file/directory sizes, and generates an HTML report rendered with React."
13
- spec.homepage = "https://example.com/file_tree_visualizer"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.7"
11
+ spec.summary = 'CLI Ruby tool to analyze file sizes and generate an interactive HTML report.'
12
+ spec.description = 'Scans a directory recursively, computes file/directory sizes, and generates an HTML report rendered with React.'
13
+ spec.homepage = 'https://github.com/JohnBidwellB/file-tree-visualizer'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 2.7'
16
16
 
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = spec.homepage
17
+ spec.metadata['source_code_uri'] = spec.homepage
18
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
19
 
20
20
  spec.files = Dir.chdir(__dir__) do
21
21
  Dir[
22
- "README.md",
23
- "Gemfile",
24
- "Rakefile",
25
- "bin/*",
26
- "lib/**/*.rb",
27
- "file_tree_visualizer.gemspec"
22
+ 'README.md',
23
+ 'Gemfile',
24
+ 'Rakefile',
25
+ 'bin/*',
26
+ 'lib/**/*.rb',
27
+ 'file_tree_visualizer.gemspec'
28
28
  ]
29
29
  end
30
- spec.bindir = "bin"
31
- spec.executables = ["file-tree-visualizer"]
32
- spec.require_paths = ["lib"]
30
+ spec.bindir = 'bin'
31
+ spec.executables = ['file-tree-visualizer']
32
+ spec.require_paths = ['lib']
33
33
  end
@@ -1,42 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "optparse"
3
+ require 'optparse'
4
4
 
5
5
  module FileTreeVisualizer
6
6
  class CLI
7
- DEFAULT_OUTPUT = "file-tree-report.html"
7
+ DEFAULT_OUTPUT = 'file-tree-report.html'
8
+ DEFAULT_LOCALE = 'en'
8
9
 
9
10
  def initialize(argv)
10
11
  @argv = argv
11
12
  end
12
13
 
13
14
  def run
14
- options, parser = parse_options(@argv.dup)
15
+ options, parser, locale = parse_options(@argv.dup)
15
16
  if options[:help]
16
17
  puts parser
17
18
  return 0
18
19
  end
19
20
 
21
+ expanded_path = File.expand_path(options[:path])
22
+ unless File.directory?(expanded_path)
23
+ raise ArgumentError,
24
+ t(locale, 'cli.errors.invalid_directory', path: expanded_path)
25
+ end
26
+
20
27
  scanner = Scanner.new(top_limit: options[:top], follow_symlinks: options[:follow_symlinks])
21
- progress = ProgressRenderer.new(enabled: options[:progress])
28
+ progress = ProgressRenderer.new(enabled: options[:progress], locale: locale)
22
29
  callback = options[:progress] ? progress.method(:render) : nil
23
- result = scanner.scan(options[:path], progress: callback)
30
+ result = scanner.scan(expanded_path, progress: callback)
24
31
  progress.finish
25
- report = ReportBuilder.new(result: result, source_path: options[:path]).build
32
+ report = ReportBuilder.new(result: result, source_path: expanded_path, locale: locale).build
26
33
 
27
34
  output_path = File.expand_path(options[:output])
28
35
  File.write(output_path, report)
29
36
 
30
- puts "Reporte generado en: #{output_path}"
31
- puts "Escaneado: #{result.stats[:directories]} directorios, #{result.stats[:files]} archivos, #{result.stats[:skipped]} omitidos."
32
- puts "Tamaño total: #{format_bytes(result.stats[:bytes])}"
37
+ puts t(locale, 'cli.messages.report_generated', path: output_path)
38
+ puts t(locale, 'cli.messages.scanned',
39
+ directories: result.stats[:directories],
40
+ files: result.stats[:files],
41
+ skipped: result.stats[:skipped])
42
+ puts t(locale, 'cli.messages.total_size', size: format_bytes(result.stats[:bytes]))
33
43
  0
34
44
  rescue OptionParser::ParseError, ArgumentError => e
45
+ locale = error_locale
35
46
  warn e.message
36
- warn "Usa --help para ver las opciones disponibles."
47
+ warn t(locale, 'cli.errors.use_help')
37
48
  1
38
49
  rescue StandardError => e
39
- warn "Error inesperado: #{e.message}"
50
+ locale = error_locale
51
+ warn t(locale, 'cli.errors.unexpected', message: e.message)
40
52
  1
41
53
  end
42
54
 
@@ -45,9 +57,10 @@ module FileTreeVisualizer
45
57
  class ProgressRenderer
46
58
  BAR_WIDTH = 28
47
59
 
48
- def initialize(io: $stdout, enabled: true)
60
+ def initialize(io: $stdout, enabled: true, locale: DEFAULT_LOCALE)
49
61
  @io = io
50
62
  @enabled = enabled && io.tty?
63
+ @locale = locale
51
64
  @last_draw_at = nil
52
65
  @has_rendered = false
53
66
  @spinner_index = 0
@@ -82,7 +95,7 @@ module FileTreeVisualizer
82
95
  def render_counting(data)
83
96
  spinner = %w[| / - \\][@spinner_index % 4]
84
97
  @spinner_index += 1
85
- @io.print(format("\rPreparando escaneo %<spinner>s Arch:%<files>d Dir:%<dirs>d",
98
+ @io.print(format("\r#{I18n.t(@locale, 'cli.progress.preparing')}",
86
99
  spinner: spinner,
87
100
  files: data.fetch(:discovered_files, 0),
88
101
  dirs: data.fetch(:discovered_directories, 0)))
@@ -102,9 +115,9 @@ module FileTreeVisualizer
102
115
  empty = BAR_WIDTH - filled
103
116
  percent = progress_ratio * 100
104
117
 
105
- @io.print(format("\rEscaneando [%<bar>s] %<percent>6.2f%% Arch:%<pf>d/%<tf>d Dir:%<pd>d/%<td>d",
106
- bar: ("#" * filled) + ("-" * empty),
107
- percent: percent,
118
+ @io.print(format("\r#{I18n.t(@locale, 'cli.progress.scanning')}",
119
+ bar: ('#' * filled) + ('-' * empty),
120
+ percent: format('%6.2f', percent),
108
121
  pf: processed_files,
109
122
  tf: total_files,
110
123
  pd: processed_directories,
@@ -113,46 +126,84 @@ module FileTreeVisualizer
113
126
  end
114
127
 
115
128
  def parse_options(argv)
129
+ locale = I18n.resolve_locale(cli_value: extract_lang_argument(argv), env_lang: ENV['LANG'])
116
130
  options = {
117
131
  path: Dir.pwd,
118
132
  output: DEFAULT_OUTPUT,
119
133
  top: Scanner::DEFAULT_TOP_LIMIT,
120
134
  follow_symlinks: false,
121
135
  progress: true,
136
+ lang: nil,
122
137
  help: false
123
138
  }
124
139
 
125
- parser = OptionParser.new do |opts|
126
- opts.banner = "Uso: file-tree-visualizer [ruta] [opciones]"
127
- opts.separator ""
128
- opts.separator "Opciones:"
140
+ parser = build_parser(locale, options)
141
+ remaining = parser.parse(argv)
142
+
143
+ if options[:lang] && !I18n.supported_locale?(options[:lang])
144
+ raise ArgumentError, t(locale,
145
+ 'cli.errors.invalid_lang',
146
+ lang: options[:lang],
147
+ supported: I18n::SUPPORTED_LOCALES.join(', '))
148
+ end
149
+
150
+ locale = I18n.resolve_locale(cli_value: options[:lang], env_lang: ENV['LANG'])
151
+ parser = build_parser(locale, options)
152
+
153
+ options[:path] = File.expand_path(remaining.first || options[:path])
154
+ raise ArgumentError, t(locale, 'cli.errors.invalid_top') unless options[:top].positive?
155
+
156
+ [options, parser, locale]
157
+ end
158
+
159
+ def build_parser(locale, options)
160
+ OptionParser.new do |opts|
161
+ opts.banner = t(locale, 'cli.banner')
162
+ opts.separator ''
163
+ opts.separator t(locale, 'cli.options_header')
129
164
 
130
- opts.on("-o", "--output FILE", "Archivo HTML de salida (default: #{DEFAULT_OUTPUT})") do |value|
165
+ opts.on('-o', '--output FILE', t(locale, 'cli.output_option', default: DEFAULT_OUTPUT)) do |value|
131
166
  options[:output] = value
132
167
  end
133
168
 
134
- opts.on("-t", "--top N", Integer, "Cantidad de archivos pesados en ranking (default: #{Scanner::DEFAULT_TOP_LIMIT})") do |value|
169
+ opts.on('-t', '--top N', Integer, t(locale, 'cli.top_option', default: Scanner::DEFAULT_TOP_LIMIT)) do |value|
135
170
  options[:top] = value
136
171
  end
137
172
 
138
- opts.on("--follow-symlinks", "Sigue enlaces simbólicos durante el escaneo") do
173
+ opts.on('--follow-symlinks', t(locale, 'cli.follow_symlinks_option')) do
139
174
  options[:follow_symlinks] = true
140
175
  end
141
176
 
142
- opts.on("--no-progress", "Desactiva la barra de progreso") do
177
+ opts.on('--no-progress', t(locale, 'cli.no_progress_option')) do
143
178
  options[:progress] = false
144
179
  end
145
180
 
146
- opts.on("-h", "--help", "Muestra esta ayuda") do
181
+ opts.on('--lang LANG', t(locale, 'cli.lang_option')) do |value|
182
+ options[:lang] = value
183
+ end
184
+
185
+ opts.on('-h', '--help', t(locale, 'cli.help_option')) do
147
186
  options[:help] = true
148
187
  end
149
188
  end
189
+ end
150
190
 
151
- remaining = parser.parse(argv)
152
- options[:path] = File.expand_path(remaining.first || options[:path])
153
- raise ArgumentError, "--top debe ser mayor que 0" unless options[:top].positive?
191
+ def extract_lang_argument(argv)
192
+ argv.each_with_index do |arg, index|
193
+ return arg.split('=', 2).last if arg.start_with?('--lang=')
194
+ next unless arg == '--lang'
195
+
196
+ return argv[index + 1]
197
+ end
198
+ nil
199
+ end
200
+
201
+ def t(locale, key, **vars)
202
+ I18n.t(locale, key, **vars)
203
+ end
154
204
 
155
- [options, parser]
205
+ def error_locale
206
+ I18n.resolve_locale(cli_value: extract_lang_argument(@argv), env_lang: ENV['LANG'])
156
207
  end
157
208
 
158
209
  def format_bytes(bytes)
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileTreeVisualizer
4
+ module I18n
5
+ SUPPORTED_LOCALES = %w[en es].freeze
6
+
7
+ TRANSLATIONS = {
8
+ 'en' => {
9
+ 'cli' => {
10
+ 'banner' => 'Usage: file-tree-visualizer [path] [options]',
11
+ 'options_header' => 'Options:',
12
+ 'output_option' => 'Output HTML file (default: %<default>s)',
13
+ 'top_option' => 'Number of largest files in ranking (default: %<default>s)',
14
+ 'follow_symlinks_option' => 'Follow symbolic links during scan',
15
+ 'no_progress_option' => 'Disable progress bar',
16
+ 'lang_option' => 'Language for CLI and report (en|es)',
17
+ 'help_option' => 'Show this help',
18
+ 'messages' => {
19
+ 'report_generated' => 'Report generated at: %<path>s',
20
+ 'scanned' => 'Scanned: %<directories>s directories, %<files>s files, %<skipped>s skipped.',
21
+ 'total_size' => 'Total size: %<size>s'
22
+ },
23
+ 'errors' => {
24
+ 'invalid_top' => '--top must be greater than 0',
25
+ 'invalid_lang' => "Unsupported language '%<lang>s'. Supported values: %<supported>s",
26
+ 'invalid_directory' => 'Path is not a directory: %<path>s',
27
+ 'use_help' => 'Use --help to see available options.',
28
+ 'unexpected' => 'Unexpected error: %<message>s'
29
+ },
30
+ 'progress' => {
31
+ 'preparing' => 'Preparing scan %<spinner>s Files:%<files>s Dirs:%<dirs>s',
32
+ 'scanning' => 'Scanning [%<bar>s] %<percent>s%% Files:%<pf>s/%<tf>s Dirs:%<pd>s/%<td>s'
33
+ }
34
+ },
35
+ 'report' => {
36
+ 'page_title' => 'File Report',
37
+ 'header_title' => 'Disk usage report',
38
+ 'meta_source_prefix' => 'Source: ',
39
+ 'meta_generated_prefix' => 'Generated: ',
40
+ 'stats' => {
41
+ 'total_size' => 'Total size',
42
+ 'files' => 'Files',
43
+ 'directories' => 'Directories',
44
+ 'skipped' => 'Skipped entries'
45
+ },
46
+ 'filters' => {
47
+ 'search_label' => 'Search by name/path',
48
+ 'search_placeholder' => 'eg: node_modules, .mp4, backup',
49
+ 'min_size_label' => 'Minimum size (MB)',
50
+ 'min_size_hint' => 'Filters tree nodes and ranking by minimum size.'
51
+ },
52
+ 'panels' => {
53
+ 'tree' => 'File tree',
54
+ 'top_files' => 'Top largest files'
55
+ },
56
+ 'table' => {
57
+ 'index' => '#',
58
+ 'size' => 'Size',
59
+ 'path' => 'Path'
60
+ },
61
+ 'empty' => {
62
+ 'tree' => 'No results with current filters.',
63
+ 'top_files' => 'No files to show with current filters.'
64
+ }
65
+ }
66
+ },
67
+ 'es' => {
68
+ 'cli' => {
69
+ 'banner' => 'Uso: file-tree-visualizer [ruta] [opciones]',
70
+ 'options_header' => 'Opciones:',
71
+ 'output_option' => 'Archivo HTML de salida (default: %<default>s)',
72
+ 'top_option' => 'Cantidad de archivos pesados en ranking (default: %<default>s)',
73
+ 'follow_symlinks_option' => 'Sigue enlaces simbólicos durante el escaneo',
74
+ 'no_progress_option' => 'Desactiva la barra de progreso',
75
+ 'lang_option' => 'Idioma para CLI y reporte (en|es)',
76
+ 'help_option' => 'Muestra esta ayuda',
77
+ 'messages' => {
78
+ 'report_generated' => 'Reporte generado en: %<path>s',
79
+ 'scanned' => 'Escaneado: %<directories>s directorios, %<files>s archivos, %<skipped>s omitidos.',
80
+ 'total_size' => 'Tamano total: %<size>s'
81
+ },
82
+ 'errors' => {
83
+ 'invalid_top' => '--top debe ser mayor que 0',
84
+ 'invalid_lang' => "Idioma no soportado '%<lang>s'. Valores permitidos: %<supported>s",
85
+ 'invalid_directory' => 'La ruta no es un directorio: %<path>s',
86
+ 'use_help' => 'Usa --help para ver las opciones disponibles.',
87
+ 'unexpected' => 'Error inesperado: %<message>s'
88
+ },
89
+ 'progress' => {
90
+ 'preparing' => 'Preparando escaneo %<spinner>s Arch:%<files>s Dir:%<dirs>s',
91
+ 'scanning' => 'Escaneando [%<bar>s] %<percent>s%% Arch:%<pf>s/%<tf>s Dir:%<pd>s/%<td>s'
92
+ }
93
+ },
94
+ 'report' => {
95
+ 'page_title' => 'Reporte de archivos',
96
+ 'header_title' => 'Reporte de espacio en disco',
97
+ 'meta_source_prefix' => 'Origen: ',
98
+ 'meta_generated_prefix' => 'Generado: ',
99
+ 'stats' => {
100
+ 'total_size' => 'Tamano total',
101
+ 'files' => 'Archivos',
102
+ 'directories' => 'Directorios',
103
+ 'skipped' => 'Entradas omitidas'
104
+ },
105
+ 'filters' => {
106
+ 'search_label' => 'Buscar por nombre/ruta',
107
+ 'search_placeholder' => 'ej: node_modules, .mp4, backup',
108
+ 'min_size_label' => 'Tamano minimo (MB)',
109
+ 'min_size_hint' => 'Filtra nodos y ranking por tamano minimo.'
110
+ },
111
+ 'panels' => {
112
+ 'tree' => 'Arbol de archivos',
113
+ 'top_files' => 'Top archivos mas pesados'
114
+ },
115
+ 'table' => {
116
+ 'index' => '#',
117
+ 'size' => 'Tamano',
118
+ 'path' => 'Ruta'
119
+ },
120
+ 'empty' => {
121
+ 'tree' => 'Sin resultados con los filtros actuales.',
122
+ 'top_files' => 'Sin archivos para mostrar con los filtros actuales.'
123
+ }
124
+ }
125
+ }
126
+ }.freeze
127
+
128
+ module_function
129
+
130
+ def supported_locale?(value)
131
+ SUPPORTED_LOCALES.include?(normalize_locale(value))
132
+ end
133
+
134
+ def resolve_locale(cli_value: nil, env_lang: nil)
135
+ normalize_locale(cli_value) || normalize_locale(env_lang) || 'en'
136
+ end
137
+
138
+ def normalize_locale(value)
139
+ return nil if value.nil?
140
+
141
+ normalized = value.to_s.strip.downcase
142
+ return nil if normalized.empty?
143
+
144
+ return 'es' if normalized.start_with?('es')
145
+ return 'en' if normalized.start_with?('en')
146
+
147
+ nil
148
+ end
149
+
150
+ def t(locale, key, **vars)
151
+ template = lookup(locale, key) || lookup('en', key)
152
+ return key unless template.is_a?(String)
153
+
154
+ return template if vars.empty?
155
+
156
+ template % vars.transform_keys(&:to_sym)
157
+ rescue KeyError
158
+ template
159
+ end
160
+
161
+ def lookup(locale, key)
162
+ current = TRANSLATIONS[locale]
163
+ return nil unless current
164
+
165
+ key.to_s.split('.').each do |part|
166
+ return nil unless current.is_a?(Hash)
167
+
168
+ current = current[part]
169
+ end
170
+ current
171
+ end
172
+ end
173
+ end
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "time"
3
+ require 'json'
4
+ require 'time'
5
5
 
6
6
  module FileTreeVisualizer
7
7
  class ReportBuilder
8
- def initialize(result:, source_path:, generated_at: Time.now.utc)
8
+ def initialize(result:, source_path:, generated_at: Time.now.utc, locale: 'en')
9
9
  @result = result
10
10
  @source_path = File.expand_path(source_path.to_s)
11
11
  @generated_at = generated_at
12
+ @locale = I18n.resolve_locale(cli_value: locale)
12
13
  end
13
14
 
14
15
  def build
15
- payload_json = JSON.generate(payload).gsub("</", "<\\/")
16
+ payload_json = JSON.generate(payload).gsub('</', '<\\/')
17
+ translations_json = JSON.generate(translation_payload).gsub('</', '<\\/')
16
18
  <<~HTML
17
19
  <!doctype html>
18
- <html lang="es">
20
+ <html lang="#{@locale}">
19
21
  <head>
20
22
  <meta charset="utf-8" />
21
23
  <meta name="viewport" content="width=device-width, initial-scale=1" />
22
- <title>Reporte de archivos</title>
24
+ <title>#{t('report.page_title')}</title>
23
25
  <style>
24
26
  :root {
25
27
  color-scheme: dark;
@@ -215,6 +217,7 @@ module FileTreeVisualizer
215
217
  <script>
216
218
  (function () {
217
219
  const data = JSON.parse(document.getElementById("scan-data").textContent);
220
+ const i18n = #{translations_json};
218
221
  const app = document.getElementById("app");
219
222
 
220
223
  const state = {
@@ -256,31 +259,31 @@ module FileTreeVisualizer
256
259
  '<div class="container">',
257
260
  ' <header class="header">',
258
261
  ' <div>',
259
- ' <h1>Reporte de espacio en disco</h1>',
262
+ ' <h1>' + i18n.headerTitle + '</h1>',
260
263
  ' <div class="meta" id="meta-source"></div>',
261
264
  ' <div class="meta" id="meta-generated"></div>',
262
265
  ' </div>',
263
266
  ' </header>',
264
267
  ' <section class="stats">',
265
- ' <div class="stat"><div class="stat-label">Tamaño total</div><div class="stat-value" id="stat-bytes"></div></div>',
266
- ' <div class="stat"><div class="stat-label">Archivos</div><div class="stat-value" id="stat-files"></div></div>',
267
- ' <div class="stat"><div class="stat-label">Directorios</div><div class="stat-value" id="stat-directories"></div></div>',
268
- ' <div class="stat"><div class="stat-label">Entradas omitidas</div><div class="stat-value" id="stat-skipped"></div></div>',
268
+ ' <div class="stat"><div class="stat-label">' + i18n.statsTotalSize + '</div><div class="stat-value" id="stat-bytes"></div></div>',
269
+ ' <div class="stat"><div class="stat-label">' + i18n.statsFiles + '</div><div class="stat-value" id="stat-files"></div></div>',
270
+ ' <div class="stat"><div class="stat-label">' + i18n.statsDirectories + '</div><div class="stat-value" id="stat-directories"></div></div>',
271
+ ' <div class="stat"><div class="stat-label">' + i18n.statsSkipped + '</div><div class="stat-value" id="stat-skipped"></div></div>',
269
272
  ' </section>',
270
273
  ' <section class="filters">',
271
- ' <label>Buscar por nombre/ruta<input id="query-input" type="text" placeholder="ej: node_modules, .mp4, backup" /></label>',
272
- ' <label>Tamaño mínimo (MB)<input id="min-size-input" type="number" min="0" value="0" /><span class="hint">Filtra nodos y ranking por tamaño mínimo.</span></label>',
274
+ ' <label>' + i18n.searchLabel + '<input id="query-input" type="text" placeholder="' + i18n.searchPlaceholder + '" /></label>',
275
+ ' <label>' + i18n.minSizeLabel + '<input id="min-size-input" type="number" min="0" value="0" /><span class="hint">' + i18n.minSizeHint + '</span></label>',
273
276
  ' </section>',
274
277
  ' <section class="layout">',
275
278
  ' <article class="panel">',
276
- ' <h2>Árbol de archivos</h2>',
279
+ ' <h2>' + i18n.treeTitle + '</h2>',
277
280
  ' <div class="panel-body" id="tree-panel"></div>',
278
281
  ' </article>',
279
282
  ' <article class="panel">',
280
- ' <h2>Top archivos más pesados</h2>',
283
+ ' <h2>' + i18n.topFilesTitle + '</h2>',
281
284
  ' <div class="panel-body">',
282
285
  ' <table>',
283
- ' <thead><tr><th>#</th><th>Tamaño</th><th>Ruta</th></tr></thead>',
286
+ ' <thead><tr><th>' + i18n.tableIndex + '</th><th>' + i18n.tableSize + '</th><th>' + i18n.tablePath + '</th></tr></thead>',
284
287
  ' <tbody id="top-files-body"></tbody>',
285
288
  ' </table>',
286
289
  ' </div>',
@@ -294,8 +297,8 @@ module FileTreeVisualizer
294
297
  const treePanel = document.getElementById("tree-panel");
295
298
  const topFilesBody = document.getElementById("top-files-body");
296
299
 
297
- document.getElementById("meta-source").textContent = "Origen: " + data.source_path;
298
- document.getElementById("meta-generated").textContent = "Generado: " + data.generated_at;
300
+ document.getElementById("meta-source").textContent = i18n.metaSourcePrefix + data.source_path;
301
+ document.getElementById("meta-generated").textContent = i18n.metaGeneratedPrefix + data.generated_at;
299
302
  document.getElementById("stat-bytes").textContent = formatBytes(data.stats.bytes);
300
303
  document.getElementById("stat-files").textContent = formatNumber(data.stats.files);
301
304
  document.getElementById("stat-directories").textContent = formatNumber(data.stats.directories);
@@ -365,7 +368,7 @@ module FileTreeVisualizer
365
368
  if (!nodeMatches(data.root, activeFilters.query, activeFilters.minBytes)) {
366
369
  const empty = document.createElement("div");
367
370
  empty.className = "empty-state";
368
- empty.textContent = "Sin resultados con los filtros actuales.";
371
+ empty.textContent = i18n.emptyTree;
369
372
  treePanel.appendChild(empty);
370
373
  return;
371
374
  }
@@ -384,7 +387,7 @@ module FileTreeVisualizer
384
387
  const cell = document.createElement("td");
385
388
  cell.colSpan = 3;
386
389
  cell.className = "empty-state";
387
- cell.textContent = "Sin archivos para mostrar con los filtros actuales.";
390
+ cell.textContent = i18n.emptyTopFiles;
388
391
  emptyRow.appendChild(cell);
389
392
  topFilesBody.appendChild(emptyRow);
390
393
  return;
@@ -439,5 +442,32 @@ module FileTreeVisualizer
439
442
  top_files: @result.top_files
440
443
  }
441
444
  end
445
+
446
+ def translation_payload
447
+ {
448
+ headerTitle: t('report.header_title'),
449
+ metaSourcePrefix: t('report.meta_source_prefix'),
450
+ metaGeneratedPrefix: t('report.meta_generated_prefix'),
451
+ statsTotalSize: t('report.stats.total_size'),
452
+ statsFiles: t('report.stats.files'),
453
+ statsDirectories: t('report.stats.directories'),
454
+ statsSkipped: t('report.stats.skipped'),
455
+ searchLabel: t('report.filters.search_label'),
456
+ searchPlaceholder: t('report.filters.search_placeholder'),
457
+ minSizeLabel: t('report.filters.min_size_label'),
458
+ minSizeHint: t('report.filters.min_size_hint'),
459
+ treeTitle: t('report.panels.tree'),
460
+ topFilesTitle: t('report.panels.top_files'),
461
+ tableIndex: t('report.table.index'),
462
+ tableSize: t('report.table.size'),
463
+ tablePath: t('report.table.path'),
464
+ emptyTree: t('report.empty.tree'),
465
+ emptyTopFiles: t('report.empty.top_files')
466
+ }
467
+ end
468
+
469
+ def t(key)
470
+ I18n.t(@locale, key)
471
+ end
442
472
  end
443
473
  end
@@ -14,7 +14,7 @@ module FileTreeVisualizer
14
14
  def scan(path, progress: nil)
15
15
  reset!
16
16
  expanded_path = File.expand_path(path.to_s)
17
- raise ArgumentError, "La ruta no es un directorio: #{expanded_path}" unless File.directory?(expanded_path)
17
+ raise ArgumentError, "Path is not a directory: #{expanded_path}" unless File.directory?(expanded_path)
18
18
 
19
19
  @progress_callback = progress
20
20
  notify_counting_progress(force: true)
@@ -66,7 +66,7 @@ module FileTreeVisualizer
66
66
  node = {
67
67
  name: directory_name,
68
68
  path: path,
69
- type: "directory",
69
+ type: 'directory',
70
70
  size: 0,
71
71
  children: []
72
72
  }
@@ -106,7 +106,7 @@ module FileTreeVisualizer
106
106
  file_node = {
107
107
  name: entry_name,
108
108
  path: child_path,
109
- type: "file",
109
+ type: 'file',
110
110
  size: file_size
111
111
  }
112
112
  parent_node[:children] << file_node
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FileTreeVisualizer
4
- VERSION = "0.0.3"
4
+ VERSION = '0.1.0'
5
5
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "file_tree_visualizer/version"
4
- require_relative "file_tree_visualizer/scanner"
5
- require_relative "file_tree_visualizer/report_builder"
6
- require_relative "file_tree_visualizer/cli"
3
+ require_relative 'file_tree_visualizer/version'
4
+ require_relative 'file_tree_visualizer/i18n'
5
+ require_relative 'file_tree_visualizer/scanner'
6
+ require_relative 'file_tree_visualizer/report_builder'
7
+ require_relative 'file_tree_visualizer/cli'
7
8
 
8
9
  module FileTreeVisualizer
9
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: file_tree_visualizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - jbidwell1
@@ -26,15 +26,16 @@ files:
26
26
  - file_tree_visualizer.gemspec
27
27
  - lib/file_tree_visualizer.rb
28
28
  - lib/file_tree_visualizer/cli.rb
29
+ - lib/file_tree_visualizer/i18n.rb
29
30
  - lib/file_tree_visualizer/report_builder.rb
30
31
  - lib/file_tree_visualizer/scanner.rb
31
32
  - lib/file_tree_visualizer/version.rb
32
- homepage: https://example.com/file_tree_visualizer
33
+ homepage: https://github.com/JohnBidwellB/file-tree-visualizer
33
34
  licenses:
34
35
  - MIT
35
36
  metadata:
36
- homepage_uri: https://example.com/file_tree_visualizer
37
- source_code_uri: https://example.com/file_tree_visualizer
37
+ source_code_uri: https://github.com/JohnBidwellB/file-tree-visualizer
38
+ changelog_uri: https://github.com/JohnBidwellB/file-tree-visualizer/blob/main/CHANGELOG.md
38
39
  post_install_message:
39
40
  rdoc_options: []
40
41
  require_paths: