Skip to content

Commit 21c73b9

Browse files
Automation entry locale (#2703)
* Internationalize automation justification text Add support for automation justifications to appear in each project's entry_locale instead of always in English. Changes: - Chief: Add with_project_locale method to temporarily set I18n.locale during detective execution - Chief: Wrap propose_changes in locale context to ensure explanations are generated in project's language - Add 'detectives' namespace to en.yml with all explanation strings organized by detective name (github, floss_license, hardened_sites, name_from_url, project_sites_https, repo_files, subdir_files, test_forced) - Update all detectives to use I18n.t() instead of hardcoded strings: - github_basic_detective: 14 explanation strings - floss_license_detective: 3 explanation strings with interpolation - hardened_sites_detective: 3 constants converted to methods - name_from_url_detective: 3 explanation strings - project_sites_https_detective: 4 explanation strings - repo_files_examine_detective: 6 explanation strings with interpolation - subdir_file_contents_detective: 3 explanation strings with interpolation - test_forced_detective: 1 explanation string with interpolation - build_detective: 1 explanation string shared with repo_files Implementation uses Option 1 approach: Chief temporarily sets I18n.locale to project's entry_locale in an ensure block, guaranteeing locale is restored even if exceptions occur. All 803 tests pass. English translations provided; other languages deferred for future translation service integration. Signed-off-by: David A. Wheeler <dwheeler@dwheeler.com> * Remove unneeded comment markers Signed-off-by: David A. Wheeler <dwheeler@dwheeler.com> * Add translations of automations Signed-off-by: David A. Wheeler <dwheeler@dwheeler.com> * Remove dead code Signed-off-by: David A. Wheeler <dwheeler@dwheeler.com> --------- Signed-off-by: David A. Wheeler <dwheeler@dwheeler.com>
1 parent bdf05c2 commit 21c73b9

28 files changed

+868
-80
lines changed

app/lib/build_detective.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ def files_named(name_pattern)
2828
def met_result(result_description, html_url)
2929
{
3030
value: CriterionStatus::MET, confidence: 3,
31-
explanation:
32-
"Non-trivial #{result_description} file in repository: " \
33-
"<#{html_url}>."
31+
explanation: I18n.t('detectives.repo_files.file_found',
32+
description: result_description, url: html_url)
3433
}
3534
end
3635

app/lib/chief.rb

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ def log_detective_failure(source, e, detective, proposal, data)
124124
end
125125
end
126126

127+
# Execute a block with I18n.locale temporarily set to the project's entry_locale.
128+
# This ensures detective explanations are generated in the project's language.
129+
# @yield Block to execute with project locale set
130+
# @return Result of the block
131+
def with_project_locale
132+
original_locale = I18n.locale
133+
I18n.locale = @entry_locale&.to_sym || :en
134+
yield
135+
ensure
136+
I18n.locale = original_locale
137+
end
138+
127139
# Invoke one "Detective", which will
128140
# analyze the project and reply with an updated changeset in the form
129141
# { fieldname1: { value: value, confidence: 1..5, explanation: text}, ...}
@@ -264,24 +276,26 @@ def needed_outputs(needed_fields, changed_fields = nil)
264276
# @param changed_fields [Array<Symbol>, nil] Fields that were explicitly changed
265277
# rubocop:disable Metrics/MethodLength
266278
def propose_changes(needed_fields: nil, changed_fields: nil)
267-
current_proposal = {} # Current best changeset.
279+
with_project_locale do
280+
current_proposal = {} # Current best changeset.
268281

269-
# Determine what outputs we need
270-
needed = needed_outputs(needed_fields, changed_fields)
282+
# Determine what outputs we need
283+
needed = needed_outputs(needed_fields, changed_fields)
271284

272-
# Filter to only needed detectives (subset varies per request)
273-
detectives_to_run = filter_needed_detectives(needed)
285+
# Filter to only needed detectives (subset varies per request)
286+
detectives_to_run = filter_needed_detectives(needed)
274287

275-
# Sort that specific subset in dependency order
276-
detectives_to_run = topological_sort_detectives(detectives_to_run)
288+
# Sort that specific subset in dependency order
289+
detectives_to_run = topological_sort_detectives(detectives_to_run)
277290

278-
# Run each detective in the sorted order
279-
detectives_to_run.each do |detective_class|
280-
detective = detective_class.new
281-
detective.octokit_client_factory = @client_factory
282-
current_proposal = propose_one_change(detective, current_proposal)
291+
# Run each detective in the sorted order
292+
detectives_to_run.each do |detective_class|
293+
detective = detective_class.new
294+
detective.octokit_client_factory = @client_factory
295+
current_proposal = propose_one_change(detective, current_proposal)
296+
end
297+
current_proposal
283298
end
284-
current_proposal
285299
end
286300
# rubocop:enable Metrics/MethodLength
287301

app/lib/floss_license_detective.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,21 +148,20 @@ def analyze(_evidence, current)
148148
floss_license_osi_status:
149149
{
150150
value: CriterionStatus::MET, confidence: 5,
151-
explanation: "The #{license} license is approved by the " \
152-
'Open Source Initiative (OSI).'
151+
explanation: I18n.t('detectives.floss_license.osi_approved',
152+
license: license)
153153
},
154154
floss_license_status:
155155
{
156156
value: CriterionStatus::MET, confidence: 5,
157-
explanation: "The #{license} license is approved by the " \
158-
'Open Source Initiative (OSI).'
157+
explanation: I18n.t('detectives.floss_license.osi_approved',
158+
license: license)
159159
},
160160
osps_le_02_01_status:
161161
{
162162
value: CriterionStatus::MET, confidence: 5,
163-
explanation: "The #{license} license for the repository " \
164-
'contents is approved by the ' \
165-
'Open Source Initiative (OSI).'
163+
explanation: I18n.t('detectives.floss_license.osi_approved_repository',
164+
license: license)
166165
},
167166
# We can't know what the license of the *released* assets are
168167
# looking only at the repository, as the LICENSE file is focused
@@ -176,7 +175,7 @@ def analyze(_evidence, current)
176175
floss_license_osi_status:
177176
{
178177
value: CriterionStatus::UNMET, confidence: 1,
179-
explanation: '// Did not find license in the OSI list.'
178+
explanation: I18n.t('detectives.floss_license.not_in_osi_list')
180179
}
181180
}
182181
else

app/lib/github_basic_detective.rb

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -101,57 +101,50 @@ def analyze(_evidence, current)
101101
# We have a github repo.
102102
results[:repo_public_status] = {
103103
value: CriterionStatus::MET, confidence: 3,
104-
explanation: 'Repository on GitHub, which provides ' \
105-
'public git repositories with URLs.'
104+
explanation: I18n.t('detectives.github.repo_public')
106105
}
107106
results[:repo_track_status] = {
108107
value: CriterionStatus::MET, confidence: 4,
109-
explanation: 'Repository on GitHub, which uses git. ' \
110-
'git can track the changes, ' \
111-
'who made them, and when they were made.'
108+
explanation: I18n.t('detectives.github.repo_track')
112109
}
113110
results[:repo_distributed_status] = {
114111
value: CriterionStatus::MET, confidence: 4,
115-
explanation: 'Repository on GitHub, which uses git. ' \
116-
'git is distributed.'
112+
explanation: I18n.t('detectives.github.repo_distributed')
117113
}
118114
results[:contribution_status] = {
119115
value: CriterionStatus::MET, confidence: 2,
120-
explanation: 'Projects on GitHub by default use issues and ' \
121-
'pull requests, as encouraged by documentation such as ' \
122-
'<https://guides.github.com/activities/' \
123-
'contributing-to-open-source/>.'
116+
explanation: I18n.t('detectives.github.contribution')
124117
}
125118
results[:discussion_status] = {
126119
value: CriterionStatus::MET, confidence: 3,
127-
explanation: 'GitHub supports discussions on issues and pull requests.'
120+
explanation: I18n.t('detectives.github.discussion')
128121
}
129122
# Baseline: public discussion mechanisms (same evidence as discussion_status)
130123
results[:osps_gv_02_01_status] = {
131124
value: CriterionStatus::MET, confidence: 3,
132-
explanation: 'GitHub supports public discussions on proposed changes (via pull requests) and usage obstacles (via issues).'
125+
explanation: I18n.t('detectives.github.osps_gv_02_01')
133126
}
134127
# Baseline criteria - defect reporting instructions (low confidence)
135128
results[:osps_do_02_01_status] = {
136129
value: CriterionStatus::MET, confidence: 2,
137-
explanation: 'GitHub provides defect reporting mechanisms by default (via issues).'
130+
explanation: I18n.t('detectives.github.osps_do_02_01')
138131
}
139132
# 2FA required by GitHub. An organization *might* use multiple repo
140133
# hosts, but given our information, 2FA seems highly likely.
141134
results[:osps_ac_01_01_status] = {
142135
value: CriterionStatus::MET, confidence: 3,
143-
explanation: 'GitHub requires 2FA as of March 2023.'
136+
explanation: I18n.t('detectives.github.osps_ac_01_01')
144137
}
145138
# Publicly readable, if we can read it. It's possible it's not current,
146139
# but if this really is the "main" repo (as claimed) then this is met.
147140
results[:osps_qa_01_01_status] = {
148141
value: CriterionStatus::MET, confidence: 3,
149-
explanation: 'Repository is publicly available on GitHub.'
142+
explanation: I18n.t('detectives.github.osps_qa_01_01')
150143
}
151144
# If the main repo is on GitHub, then git will store this
152145
results[:osps_qa_01_02_status] = {
153146
value: CriterionStatus::MET, confidence: 3,
154-
explanation: 'Repository git metadata is publicly available on GitHub.'
147+
explanation: I18n.t('detectives.github.osps_qa_01_02')
155148
}
156149

157150
# Get basic evidence
@@ -169,15 +162,15 @@ def analyze(_evidence, current)
169162
if basic_repo_data[:name]
170163
results[:name] = {
171164
value: basic_repo_data[:name],
172-
confidence: 3, explanation: 'GitHub name'
165+
confidence: 3, explanation: I18n.t('detectives.github.name')
173166
}
174167
end
175168
if basic_repo_data[:description]
176169
results[:description] = {
177170
value: basic_repo_data[:description].gsub(
178171
/(\A|\s)\:[a-zA-Z]+\:(\s|\Z)/, ' '
179172
).strip,
180-
confidence: 3, explanation: 'GitHub description'
173+
confidence: 3, explanation: I18n.t('detectives.github.description')
181174
}
182175
end
183176
# rubocop:enable Metrics/BlockLength
@@ -194,7 +187,7 @@ def analyze(_evidence, current)
194187
license = cleanup_license(license_data_raw[:key])
195188
results[:license] = {
196189
value: license,
197-
confidence: 3, explanation: 'GitHub API license analysis'
190+
confidence: 3, explanation: I18n.t('detectives.github.license')
198191
}
199192
end
200193

@@ -204,7 +197,7 @@ def analyze(_evidence, current)
204197
results[:implementation_languages] = {
205198
value: implementation_languages,
206199
confidence: 3,
207-
explanation: 'GitHub API implementation language analysis'
200+
explanation: I18n.t('detectives.github.implementation_languages')
208201
}
209202
end
210203

app/lib/hardened_sites_detective.rb

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,20 @@ class HardenedSitesDetective < Detective
1717
content-security-policy strict-transport-security
1818
x-content-type-options
1919
].freeze
20-
MET =
20+
21+
def met_result
2122
{
2223
value: CriterionStatus::MET, confidence: 3,
23-
explanation: 'Found all required security hardening headers.'
24-
}.freeze
25-
UNMET_MISSING =
26-
{
27-
value: CriterionStatus::UNMET, confidence: 5,
28-
explanation: 'Required security hardening headers missing: '
29-
}.freeze
30-
UNMET_NOSNIFF =
24+
explanation: I18n.t('detectives.hardened_sites.all_headers_found')
25+
}
26+
end
27+
28+
def unmet_missing_result
3129
{
3230
value: CriterionStatus::UNMET, confidence: 5,
33-
explanation: '// X-Content-Type-Options was not set to "nosniff".'
34-
}.freeze
31+
explanation: I18n.t('detectives.hardened_sites.headers_missing')
32+
}
33+
end
3534

3635
INPUTS = %i[repo_url homepage_url].freeze
3736
OUTPUTS = [:hardened_site_status].freeze
@@ -114,9 +113,9 @@ def report_on_check_urls(evidence, homepage_url, repo_url)
114113
all_problems = problems_in_urls(evidence, urls)
115114
results[:hardened_site_status] =
116115
if all_problems.empty?
117-
MET
116+
met_result
118117
else
119-
answer = UNMET_MISSING.deep_dup # clone but result is not frozen
118+
answer = unmet_missing_result
120119
answer[:explanation] += all_problems.join(', ')
121120
answer
122121
end

app/lib/name_from_url_detective.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ def analyze(_evidence, current)
3232
@results[:name] =
3333
{
3434
value: finding[2], confidence: 1,
35-
explanation: "The project URL's domain name suggests this."
35+
explanation: I18n.t('detectives.name_from_url.domain_suggests')
3636
}
3737
else
3838
finding = name_in_url_tail.match(homepage_url)
3939
if finding
4040
@results[:name] =
4141
{
4242
value: finding[1], confidence: 1,
43-
explanation: "The project URL's tail suggests this."
43+
explanation: I18n.t('detectives.name_from_url.tail_suggests')
4444
}
4545
end
4646
end
@@ -51,7 +51,7 @@ def analyze(_evidence, current)
5151
@results[:name] =
5252
{
5353
value: finding[1], confidence: 1,
54-
explanation: "The repo URL's tail suggests this."
54+
explanation: I18n.t('detectives.name_from_url.repo_tail_suggests')
5555
}
5656
end
5757
end

app/lib/project_sites_https_detective.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ def set_http_results
4343
@results[:sites_https_status] =
4444
{
4545
value: CriterionStatus::UNMET, confidence: 5,
46-
explanation: '// Given an http: URL.'
46+
explanation: I18n.t('detectives.project_sites_https.given_http')
4747
}
4848
# Any official channel using http is a problem
4949
@results[:osps_br_03_01_status] =
5050
{
5151
value: CriterionStatus::UNMET, confidence: 5,
52-
explanation: 'Project URLs lists http (not https) as official.'
52+
explanation: I18n.t('detectives.project_sites_https.official_http')
5353
}
5454
# We don't know enough to be *certain* what the distribution channels
5555
# are; it's possible that the official distribution channels *are*
@@ -59,7 +59,7 @@ def set_http_results
5959
@results[:osps_br_03_02_status] =
6060
{
6161
value: CriterionStatus::UNMET, confidence: 3,
62-
explanation: 'We were given a URL that uses http (not https).'
62+
explanation: I18n.t('detectives.project_sites_https.url_uses_http')
6363
}
6464
end
6565
# rubocop:enable Metrics/MethodLength
@@ -69,7 +69,8 @@ def met_result(explanation)
6969
end
7070

7171
def set_https_results
72-
@results[:sites_https_status] = met_result('Given only https: URLs.')
72+
@results[:sites_https_status] =
73+
met_result(I18n.t('detectives.project_sites_https.given_https'))
7374
@results[:osps_br_03_01_status] =
7475
met_result('Project URLs use HTTPS exclusively.')
7576
@results[:osps_br_03_02_status] =

app/lib/repo_files_examine_detective.rb

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,16 @@ def directory_named(name_pattern)
4949
def unmet_result(result_description, confidence: 1)
5050
{
5151
value: CriterionStatus::UNMET, confidence: confidence,
52-
explanation: "// No #{result_description} file found."
52+
explanation: I18n.t('detectives.repo_files.no_file_found',
53+
description: result_description)
5354
}
5455
end
5556

5657
def met_result(result_description, html_url, confidence: 3)
5758
{
5859
value: CriterionStatus::MET, confidence: confidence,
59-
explanation:
60-
"Non-trivial #{result_description} file in repository: " \
61-
"<#{html_url}>."
60+
explanation: I18n.t('detectives.repo_files.file_found',
61+
description: result_description, url: html_url)
6262
}
6363
end
6464

@@ -135,7 +135,7 @@ def set_baseline_contribution_status
135135
confidence = @results[:contribution_status][:confidence]
136136
@results[:osps_gv_03_01_status] = {
137137
value: CriterionStatus::MET, confidence: confidence,
138-
explanation: 'Contribution process documented in repository.'
138+
explanation: I18n.t('detectives.repo_files.contribution_documented')
139139
}
140140
end
141141

@@ -164,17 +164,16 @@ def set_baseline_license_status
164164
confidence = @results[:license_location_status][:confidence]
165165
@results[:osps_le_03_01_status] = {
166166
value: CriterionStatus::MET, confidence: confidence,
167-
explanation: 'License file found in repository.'
167+
explanation: I18n.t('detectives.repo_files.license_found')
168168
}
169169
else
170170
@results[:osps_le_03_01_status] = {
171171
value: CriterionStatus::UNMET, confidence: 5,
172-
explanation: 'License file not found in repository.'
172+
explanation: I18n.t('detectives.repo_files.license_not_found')
173173
}
174174
@results[:osps_le_03_02_status] = {
175175
value: CriterionStatus::UNMET, confidence: 2,
176-
explanation: 'License file not found in repository (likely not ' \
177-
'included in releases).'
176+
explanation: I18n.t('detectives.repo_files.license_not_in_releases')
178177
}
179178
end
180179
end

app/lib/subdir_file_contents_detective.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@ class SubdirFileContentsDetective < Detective
2424
def unmet_result(result_description)
2525
{
2626
value: CriterionStatus::UNMET, confidence: 1,
27-
explanation: "// No #{result_description} file(s) found."
27+
explanation: I18n.t('detectives.subdir_files.no_files_found',
28+
description: result_description)
2829
}
2930
end
3031

3132
def unmet_result_folder(result_description)
3233
{
3334
value: CriterionStatus::UNMET, confidence: 3,
34-
explanation: "// No appropriate folder found for #{result_description}."
35+
explanation: I18n.t('detectives.subdir_files.no_folder_found',
36+
description: result_description)
3537
}
3638
end
3739

3840
def met_result(result_description)
3941
{
4042
value: CriterionStatus::MET, confidence: 3,
41-
explanation:
42-
"Some #{result_description} file contents found."
43+
explanation: I18n.t('detectives.subdir_files.some_contents_found',
44+
description: result_description)
4345
}
4446
end
4547

0 commit comments

Comments
 (0)