imap-backup 16.4.2 → 16.6.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: fc471fce2bd514e0eee4cec922e8f368df0b2fb99f8b89b2d852e07799a2be77
4
- data.tar.gz: fd563c2ee648d5e384a10cdfc79a7d1d5be128bdce5908711396d414c801173f
3
+ metadata.gz: 325c46cce0eff42c96d4c44e686b32f0d52352cdfa038c17a132069b71bdf11e
4
+ data.tar.gz: 349426301b37fa38db3da6c2d71a3979892345828b88ffa2b52752a559f6a95b
5
5
  SHA512:
6
- metadata.gz: 36088caab383a7ff2528d8ad83813f58918abda4d80a56b0f2b2f1f45d5fc840934d1bc31e1907de11cc665f5aecb5b12152bb8dee686fb656294eb55440897e
7
- data.tar.gz: 389a82b7755342dd591fd0e2d45d760eb8350f697d46414d8d90776624bd0ee7263acfe163ab1e8dd42c869d12a544c2d5faa4656d4ef5c802f4991b9f33c1b4
6
+ metadata.gz: 6c4735412c68747d5365048e7748cc849bb1302a2da6e4af60354bf2a44c3c4dab5f14cf9ea7111cd7c6f416faea8ad429657a551755167662998e20ff7aae3a
7
+ data.tar.gz: 213bc6df849b8e7d72ded14920695e1a158d8657ac0d3bcd6632068f0c76258d2b5d0f37a5073867cf1f549249b1a2df651a0327ce803051311ed33fe5e757a0
data/bin/imap-backup CHANGED
@@ -11,6 +11,9 @@ end
11
11
 
12
12
  require "imap/backup/cli"
13
13
  require "imap/backup/logger"
14
+ require "imap/backup/translator"
15
+
16
+ Imap::Backup::Translator.new.setup
14
17
 
15
18
  Imap::Backup::Logger.sanitize_stderr do
16
19
  Imap::Backup::CLI.start(ARGV)
data/docs/TODO.md ADDED
@@ -0,0 +1,35 @@
1
+ # Add Internationalization (i18n) Support
2
+
3
+ Status: [x]
4
+
5
+ ## Description
6
+
7
+ Add internationalization support to make imap-backup accessible to non-English-speaking users. The primary focus should be on the menu-driven setup interface, which contains the most user-facing text. This will allow the application to display messages and prompts in the user's preferred language based on locale environment variables (LANGUAGE, LANG, LC_*).
8
+
9
+ ## Technical Specifics
10
+
11
+ - Use the `locale` and `i18n` gems
12
+ - Override the `locale` gem's behaviour when LANG=C (use `:en`)
13
+ - Store translations under `lib/imap/backup/locales`
14
+ - Prioritize translating the menu-driven setup interface (`lib/imap/backup/setup.rb` and related files)
15
+ - Extract all user-facing strings into translation files with locale "en"
16
+ - Add Italian translations
17
+
18
+ # Add Localized Help to All Setup Screens
19
+
20
+ Status: [x]
21
+
22
+ ## Description
23
+
24
+ Currently, only the Download Strategy Chooser screen has localized help functionality (via the `show_help` method). This TODO involves adding similar localized help to all other setup screens in the menu-driven interface to provide users with context-sensitive assistance in their preferred language.
25
+
26
+ ## Technical Specifics
27
+
28
+ - The Download Strategy Chooser at [lib/imap/backup/setup/global_options/download_strategy_chooser.rb](lib/imap/backup/setup/global_options/download_strategy_chooser.rb#L52-L56) already implements localized help as a reference implementation
29
+ - Screens that need localized help added:
30
+ - Main menu ([lib/imap/backup/setup.rb](lib/imap/backup/setup.rb))
31
+ - Account setup menu ([lib/imap/backup/setup/account.rb](lib/imap/backup/setup/account.rb))
32
+ - Global options menu ([lib/imap/backup/setup/global_options.rb](lib/imap/backup/setup/global_options.rb))
33
+ - Other setup screens (folder chooser, backup path, etc.)
34
+ - Add translation keys for help text to locale files (`lib/imap/backup/locales/en.yml` and `lib/imap/backup/locales/it.yml`)
35
+ - Follow the pattern: menu choice for "help" that displays localized help text and waits for key press
data/docs/i18n.md ADDED
@@ -0,0 +1,153 @@
1
+ # Internationalization (i18n)
2
+
3
+ imap-backup supports multiple languages through internationalization (i18n). This document explains how the system works and how to contribute translations.
4
+
5
+ ## Overview
6
+
7
+ The application automatically detects the user's preferred language from environment variables and displays messages in that language. English is the default fallback language.
8
+
9
+ ## How Locale Detection Works
10
+
11
+ The i18n system uses the `locale` gem to detect the user's preferred language by checking these environment variables in order:
12
+
13
+ 1. `LANGUAGE`
14
+ 2. `LANG`
15
+ 3. `LC_ALL`
16
+ 4. `LC_MESSAGES`
17
+ 5. Other `LC_*` variables
18
+
19
+ Special case: When `LANG=C` is set (common in minimal environments), the system uses English (`:en`).
20
+
21
+ The system selects the first detected locale that has a translation file available.
22
+
23
+ ## Translation Files
24
+
25
+ Translation files are stored in YAML format at:
26
+
27
+ ```
28
+ lib/imap/backup/locales/
29
+ ```
30
+
31
+ Each language has its own file named with the ISO 639-1 language code:
32
+
33
+ - `en.yml` - English (default)
34
+ - `it.yml` - Italian
35
+
36
+ ## File Structure
37
+
38
+ Translation files use a hierarchical structure with nested keys. Here's an example:
39
+
40
+ ```yaml
41
+ ---
42
+ en:
43
+ setup:
44
+ choose_action: "Choose an action"
45
+ main_menu:
46
+ title: "Main Menu"
47
+ add_account: "add account"
48
+ ```
49
+
50
+ ### Using Translations in Code
51
+
52
+ Translations are accessed using the `I18n.t` method with dot-separated keys:
53
+
54
+ ```ruby
55
+ I18n.t("setup.main_menu.title") # => "Main Menu"
56
+ ```
57
+
58
+ ### Interpolation
59
+
60
+ Some strings include variables using the `%{variable}` syntax:
61
+
62
+ ```yaml
63
+ en:
64
+ setup:
65
+ account:
66
+ choose_folders: "choose folders to %{action}"
67
+ ```
68
+
69
+ In code:
70
+
71
+ ```ruby
72
+ I18n.t("setup.account.choose_folders", action: "backup")
73
+ # => "choose folders to backup"
74
+ ```
75
+
76
+ ## Contributing Translations
77
+
78
+ ### Adding a New Language
79
+
80
+ To add support for a new language:
81
+
82
+ 1. **Create a new locale file** in `lib/imap/backup/locales/` using the ISO 639-1 language code (e.g., `fr.yml` for French).
83
+
84
+ 2. **Copy the structure from `en.yml`** to ensure all keys are present:
85
+
86
+ ```yaml
87
+ ---
88
+ fr:
89
+ setup:
90
+ choose_action: "Choisissez une action"
91
+ main_menu:
92
+ title: "Menu Principal"
93
+ # ... continue translating all keys
94
+ ```
95
+
96
+ 3. **Translate all strings** while keeping:
97
+ - The YAML structure unchanged
98
+ - All interpolation variables like `%{action}` in their original form
99
+ - Special formatting like keyboard shortcuts in their original form (e.g., `(q)`)
100
+
101
+ 4. **Test your translations** by setting the appropriate locale:
102
+
103
+ ```sh
104
+ LANG=fr_FR.UTF-8 imap-backup setup
105
+ ```
106
+
107
+ 5. **Submit a pull request** with your new translation file.
108
+
109
+ ### Updating Existing Translations
110
+
111
+ When updating translations:
112
+
113
+ 1. Compare your language file with `en.yml` to identify missing or changed keys
114
+ 2. Add or update the necessary translations
115
+ 3. Ensure the structure matches the current `en.yml` structure
116
+ 4. Test with your locale settings
117
+ 5. Submit a pull request
118
+
119
+ ## Translation Guidelines
120
+
121
+ - **Maintain context**: Keep in mind where text appears (menus, prompts, error messages)
122
+ - **Preserve formatting**: Keep special characters like `(q)` for keyboard shortcuts
123
+ - **Keep variables**: Don't translate interpolation variables like `%{action}`
124
+ - **Test thoroughly**: Run the setup interface to ensure translations fit properly
125
+ - **Be consistent**: Use consistent terminology throughout the translation
126
+ - **Follow conventions**: Respect language-specific conventions for punctuation and spacing
127
+
128
+ ## Current Language Support
129
+
130
+ - **English (en)**: Complete (reference implementation)
131
+ - **Italian (it)**: Complete
132
+
133
+ ## Testing Translations
134
+
135
+ To test translations in your local environment:
136
+
137
+ ```sh
138
+ # Test with a specific locale
139
+ LANG=it_IT.UTF-8 imap-backup setup
140
+
141
+ # Test with fallback to English
142
+ LANG=C imap-backup setup
143
+ ```
144
+
145
+ ## Need Help?
146
+
147
+ If you have questions about contributing translations:
148
+
149
+ - Check existing translation files (`en.yml` and `it.yml`) for examples
150
+ - Open an issue on GitHub to discuss translation conventions
151
+ - Ask for clarification on ambiguous strings
152
+
153
+ Thank you for helping make imap-backup accessible to users worldwide!
data/imap-backup.gemspec CHANGED
@@ -13,6 +13,7 @@ Gem::Specification.new do |gem|
13
13
  gem.files = %w[bin/imap-backup]
14
14
  gem.files += Dir.glob("docs/*.md")
15
15
  gem.files += Dir.glob("lib/**/*.rb")
16
+ gem.files += Dir.glob("lib/imap/backup/locales/*.yml")
16
17
  gem.files += %w[imap-backup.gemspec]
17
18
  gem.files += %w[LICENSE README.md]
18
19
 
@@ -21,6 +22,8 @@ Gem::Specification.new do |gem|
21
22
  gem.required_ruby_version = ">= 3.2"
22
23
 
23
24
  gem.add_dependency "highline"
25
+ gem.add_dependency "i18n"
26
+ gem.add_dependency "locale"
24
27
  gem.add_dependency "logger"
25
28
  gem.add_dependency "mail", "2.7.1"
26
29
  gem.add_dependency "net-imap", ">= 0.3.2"
@@ -72,9 +72,7 @@ module Imap::Backup
72
72
  erb_config_path = options[:erb_configuration]
73
73
 
74
74
  # Check mutual exclusivity
75
- if config_path && erb_config_path
76
- raise "Cannot specify both --config and --erb-configuration options"
77
- end
75
+ raise I18n.t("cli.helpers.config_mutual_exclusivity") if config_path && erb_config_path
78
76
 
79
77
  # Handle ERB configuration
80
78
  return load_erb_config(erb_config_path, options) if erb_config_path
@@ -86,7 +84,7 @@ module Imap::Backup
86
84
  exists = Configuration.exist?(path: path)
87
85
  if !exists
88
86
  expected = path || Configuration.default_pathname
89
- raise ConfigurationNotFound, "Configuration file '#{expected}' not found"
87
+ raise ConfigurationNotFound, I18n.t("cli.helpers.config_not_found", path: expected)
90
88
  end
91
89
  end
92
90
  Configuration.new(path: path)
@@ -96,7 +94,7 @@ module Imap::Backup
96
94
  # @return [Account] the Account information for the email address
97
95
  def account(config, email)
98
96
  account = config.accounts.find { |a| a.username == email }
99
- raise "#{email} is not a configured account" if !account
97
+ raise I18n.t("cli.helpers.account_not_configured", email: email) if !account
100
98
 
101
99
  account
102
100
  end
@@ -123,7 +121,7 @@ module Imap::Backup
123
121
  def load_erb_config(erb_path, _options)
124
122
  # Check if file exists
125
123
  unless File.exist?(erb_path)
126
- raise ConfigurationNotFound, "ERB configuration file '#{erb_path}' not found"
124
+ raise ConfigurationNotFound, I18n.t("cli.helpers.erb_config_not_found", path: erb_path)
127
125
  end
128
126
 
129
127
  begin
@@ -132,16 +130,16 @@ module Imap::Backup
132
130
  erb = ERB.new(erb_content)
133
131
  rendered_json = erb.result
134
132
  rescue SyntaxError => e
135
- raise "ERB template has syntax error: #{e.message}"
133
+ raise I18n.t("cli.helpers.erb_syntax_error", message: e.message)
136
134
  rescue StandardError => e
137
- raise "Error processing ERB template: #{e.message}"
135
+ raise I18n.t("cli.helpers.erb_processing_error", message: e.message)
138
136
  end
139
137
 
140
138
  # Validate rendered JSON
141
139
  begin
142
140
  JSON.parse(rendered_json)
143
141
  rescue JSON::ParserError => e
144
- raise "ERB template rendered invalid JSON: #{e.message}"
142
+ raise I18n.t("cli.helpers.erb_invalid_json", message: e.message)
145
143
  end
146
144
 
147
145
  # Create temporary file with rendered JSON
@@ -64,7 +64,7 @@ module Imap::Backup
64
64
 
65
65
  def print_check_results_as_text(results)
66
66
  results.each do |account_results|
67
- Kernel.puts "Account: #{account_results[:account]}"
67
+ Kernel.puts I18n.t("cli.local.check.account", account: account_results[:account])
68
68
  account_results[:folders].each do |folder_results|
69
69
  Kernel.puts "\t#{folder_results[:name]}: #{folder_results[:result]}"
70
70
  end
@@ -95,7 +95,7 @@ module Imap::Backup
95
95
  serializer, _folder = serialized_folders.find do |_s, f|
96
96
  f.name == folder_name
97
97
  end
98
- raise "Folder '#{folder_name}' not found" if !serializer
98
+ raise I18n.t("cli.local.folder_not_found", folder: folder_name) if !serializer
99
99
 
100
100
  case options[:format]
101
101
  when "json"
@@ -126,7 +126,7 @@ module Imap::Backup
126
126
  serializer, _folder = serialized_folders.find do |_s, f|
127
127
  f.name == folder_name
128
128
  end
129
- raise "Folder '#{folder_name}' not found" if !serializer
129
+ raise I18n.t("cli.local.folder_not_found", folder: folder_name) if !serializer
130
130
 
131
131
  uid_list = uids.split(",")
132
132
 
@@ -115,7 +115,9 @@ module Imap::Backup
115
115
  def list_namespaces(namespaces)
116
116
  Kernel.puts format(
117
117
  NAMESPACE_TEMPLATE,
118
- {name: "Name", prefix: "Prefix", delim: "Delimiter"}
118
+ {name: I18n.t("cli.remote.namespaces.name"),
119
+ prefix: I18n.t("cli.remote.namespaces.prefix"),
120
+ delim: I18n.t("cli.remote.namespaces.delimiter")}
119
121
  )
120
122
  list_namespace namespaces, :personal
121
123
  list_namespace namespaces, :other
@@ -127,7 +129,8 @@ module Imap::Backup
127
129
  if info
128
130
  Kernel.puts format(NAMESPACE_TEMPLATE, name: name, **info)
129
131
  else
130
- Kernel.puts format("%-10<name>s (Not defined)", name: name)
132
+ Kernel.puts format("%-10<name>s %<not_defined>s",
133
+ name: name, not_defined: I18n.t("cli.remote.namespaces.not_defined"))
131
134
  end
132
135
  end
133
136
 
@@ -38,7 +38,7 @@ module Imap::Backup
38
38
  account = account(config, email)
39
39
  restore(account, **restore_options)
40
40
  when email && options.key?(:accounts)
41
- raise "Missing EMAIL parameter"
41
+ raise I18n.t("cli.restore.missing_email_parameter")
42
42
  when !email && options.key?(:accounts)
43
43
  Logger.logger.info(
44
44
  "Calling restore with the --account option is deprecated, " \
@@ -42,10 +42,10 @@ module Imap::Backup
42
42
  private
43
43
 
44
44
  TEXT_COLUMNS = [
45
- {name: :folder, width: 20, alignment: :left},
46
- {name: :remote, width: 8, alignment: :right},
47
- {name: :both, width: 8, alignment: :right},
48
- {name: :local, width: 8, alignment: :right}
45
+ {name: :folder, i18n_key: "cli.stats.folder", width: 20, alignment: :left},
46
+ {name: :remote, i18n_key: "cli.stats.remote", width: 8, alignment: :right},
47
+ {name: :both, i18n_key: "cli.stats.both", width: 8, alignment: :right},
48
+ {name: :local, i18n_key: "cli.stats.local", width: 8, alignment: :right}
49
49
  ].freeze
50
50
  ALIGNMENT_FORMAT_SYMBOL = {left: "-", right: " "}.freeze
51
51
  private_constant :TEXT_COLUMNS, :ALIGNMENT_FORMAT_SYMBOL
@@ -96,7 +96,7 @@ module Imap::Backup
96
96
 
97
97
  def text_header
98
98
  titles = TEXT_COLUMNS.map do |column|
99
- format("%-#{column[:width]}s", column[:name])
99
+ format("%-#{column[:width]}s", I18n.t(column[:i18n_key]))
100
100
  end.join("|")
101
101
 
102
102
  underline = TEXT_COLUMNS.map do |column|
@@ -97,23 +97,24 @@ module Imap::Backup
97
97
  end
98
98
 
99
99
  def check_accounts!
100
- if destination_email == source_email
101
- raise "Source and destination accounts cannot be the same!"
102
- end
100
+ raise I18n.t("cli.transfer.same_account_error") if destination_email == source_email
103
101
 
104
- raise "Account '#{destination_email}' does not exist" if !destination_account
102
+ if !destination_account
103
+ raise I18n.t("cli.transfer.destination_not_found",
104
+ email: destination_email)
105
+ end
105
106
 
106
- raise "Account '#{source_email}' does not exist" if !source_account
107
+ raise I18n.t("cli.transfer.source_not_found", email: source_email) if !source_account
107
108
 
108
109
  if !source_account.available_for_migration?
109
- raise "Account '#{source_email}' is not available for migration " \
110
- "(status: #{source_account.status})"
110
+ raise I18n.t("cli.transfer.source_not_available",
111
+ email: source_email, status: source_account.status)
111
112
  end
112
113
 
113
114
  return if destination_account.available_for_migration?
114
115
 
115
- raise "Account '#{destination_email}' is not available for migration " \
116
- "(status: #{destination_account.status})"
116
+ raise I18n.t("cli.transfer.destination_not_available",
117
+ email: destination_email, status: destination_account.status)
117
118
  end
118
119
 
119
120
  def choose_prefixes_and_delimiters!
@@ -126,12 +127,10 @@ module Imap::Backup
126
127
  end
127
128
 
128
129
  def ensure_no_prefix_or_delimiter_parameters!
129
- if destination_delimiter
130
- raise "--automatic-namespaces is incompatible with --destination-delimiter"
131
- end
132
- raise "--automatic-namespaces is incompatible with --destination-prefix" if destination_prefix
133
- raise "--automatic-namespaces is incompatible with --source-delimiter" if source_delimiter
134
- raise "--automatic-namespaces is incompatible with --source-prefix" if source_prefix
130
+ raise I18n.t("cli.transfer.incompatible_destination_delimiter") if destination_delimiter
131
+ raise I18n.t("cli.transfer.incompatible_destination_prefix") if destination_prefix
132
+ raise I18n.t("cli.transfer.incompatible_source_delimiter") if source_delimiter
133
+ raise I18n.t("cli.transfer.incompatible_source_prefix") if source_prefix
135
134
  end
136
135
 
137
136
  def query_servers_for_settings
@@ -76,14 +76,17 @@ module Imap::Backup
76
76
  profile = thunderbird_profile(profile_name)
77
77
 
78
78
  if !profile
79
- raise "Thunderbird profile '#{profile_name}' not found" if profile_name
79
+ if profile_name
80
+ raise I18n.t("cli.utils.thunderbird_profile_not_found",
81
+ profile: profile_name)
82
+ end
80
83
 
81
- raise "Default Thunderbird profile not found"
84
+ raise I18n.t("cli.utils.default_thunderbird_profile_not_found")
82
85
  end
83
86
 
84
87
  serialized_folders = Account::SerializedFolders.new(account: account)
85
88
 
86
- raise "No serialized folders were found for account '#{email}'" if serialized_folders.none?
89
+ raise I18n.t("cli.utils.no_serialized_folders", email: email) if serialized_folders.none?
87
90
 
88
91
  serialized_folders.each_key do |serializer|
89
92
  Thunderbird::MailboxExporter.new(
@@ -224,7 +224,7 @@ module Imap::Backup
224
224
  # Prints the program version
225
225
  # @return [void]
226
226
  def version
227
- Kernel.puts "imap-backup #{Imap::Backup::VERSION}"
227
+ Kernel.puts I18n.t("cli.version", version: Imap::Backup::VERSION)
228
228
  end
229
229
  end
230
230
  end
@@ -16,10 +16,7 @@ module Imap::Backup
16
16
  # The default download strategy key
17
17
  DEFAULT_STRATEGY = "delay_metadata".freeze
18
18
  # The available download strategies
19
- DOWNLOAD_STRATEGIES = [
20
- {key: "direct", description: "write straight to disk"},
21
- {key: DEFAULT_STRATEGY, description: "delay writing metadata"}
22
- ].freeze
19
+ DOWNLOAD_STRATEGIES = ["direct", DEFAULT_STRATEGY].freeze
23
20
  # The current file version
24
21
  VERSION = "2.2".freeze
25
22
 
@@ -77,7 +74,7 @@ module Imap::Backup
77
74
  end
78
75
  end
79
76
 
80
- # @return [String] the cofigured download strategy
77
+ # @return [String] the configured download strategy
81
78
  def download_strategy
82
79
  ensure_loaded!
83
80
 
@@ -87,7 +84,7 @@ module Imap::Backup
87
84
  # @param value [String] the new strategy
88
85
  # @return [void]
89
86
  def download_strategy=(value)
90
- raise "Unknown strategy '#{value}'" if !DOWNLOAD_STRATEGIES.find { |s| s[:key] == value }
87
+ raise "Unknown strategy '#{value}'" if !DOWNLOAD_STRATEGIES.include?(value)
91
88
 
92
89
  ensure_loaded!
93
90
 
@@ -133,7 +130,7 @@ module Imap::Backup
133
130
  contents = File.read(pathname)
134
131
  data = JSON.parse(contents, symbolize_names: true)
135
132
  data[:download_strategy] =
136
- if DOWNLOAD_STRATEGIES.find { |s| s[:key] == data[:download_strategy] }
133
+ if DOWNLOAD_STRATEGIES.include?(data[:download_strategy])
137
134
  data[:download_strategy]
138
135
  else
139
136
  DEFAULT_STRATEGY