Skip to content

Commit aa4413a

Browse files
olivierlacanclaude
andauthored
Version + language selectors with translation-gap interstitials (#724)
* Add version + language selectors with translation-gap interstitials Split the combined picker into separate Version and Language selectors on v2 pages, and resolve every missing (language, version) pair to a generated interstitial instead of a 404 (issue #719). - config.rb: helpers for which spec versions exist per language (build-aware) plus proxy generation for every untranslated combo — capped at the published version in a production build, exposing drafts when serving locally. - partials/_locale_nav: two selects listing the full grid; combinations we don't have are marked "not translated" rather than hidden. A version pick keeps your language fixed; a language pick keeps your version fixed. - missing.html.haml: interstitial explaining the gap, with read-in-English and latest-translation links plus language-specific contribution instructions. Typing an untranslated URL (e.g. /fr/2.0.0/) now lands here, not on a 404. - Legacy (1.x) pages keep the existing combined picker until this is back-ported. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185bNtVR7Zh5nKRGCygHx7Z * Drop the redundant per-field globe on the v2 selectors The legacy single picker draws a globe via `.locales label::before`; that rule also matched the new `.locale-field` labels, so each of the two selects rendered its own globe. Scope the globe to the legacy label only — the collapse globe on `.locales-toggle` still handles the narrow-screen case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185bNtVR7Zh5nKRGCygHx7Z * Tag untranslated selector options with compact "(n/a)" The full "— X.X not translated" sentence overflowed the closed select and truncated to garbage (e.g. "2.0 — no", "Français — 2.0 not translate…"). Replace it with a short "(n/a)" tag, move the full explanation into the option's title tooltip, and dim untranslated options where browsers honour option colors. Options stay selectable so picking one still reaches the interstitial — disabling them would trap language switching on versions that only exist in English. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185bNtVR7Zh5nKRGCygHx7Z * Localize and streamline the translation-missing interstitial Present the interstitial copy in the requested language: strings live in data/interstitial.yml keyed by language code, resolved by the interstitial_string helper with English as the per-key fallback. Seeds the ten hand-authored languages; the rest fall back to English. Also streamlines the page: drop the redundant eyebrow and Help-translate button, trim the lede, keep the heading on one line via a wider block with the body copy capped to a reading measure, and reorganize the .missing CSS around a shared surface-panel and measure so declarations aren't duplicated. Removes the now-unused available_translation_for helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Backport the version+language selectors to older designs Render the shared v2 locale_nav partial on every version instead of the combined language-only picker, so 1.0.0/1.1.0 (application.css) and 0.3.0 (legacy.css) also get the accessible dual version+language selectors. Style the two selects in each design and hide the v2-only narrow-screen toggle; the nav JS already navigates generically off '.locales select', so no JS change is needed. Spec content, the older designs, and the single v2 interstitial are unchanged. Also drops dead '.locales li' rules left from an earlier list-based picker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add stylelint CSS linting, enforced in CI Lint the hand-authored CSS with stylelint-config-standard for correctness (duplicate selectors/properties, redundant longhands, invalid values, empty blocks), with the stylistic opinions that fight this file's deliberate style switched off and documented in .stylelintrc.cjs. Adds lint:css / lint:css:fix scripts and a css-lint workflow that fails the build on any violation. The two real issues it flagged (a triple-defined .article-body > .content and a place-self longhand) are fixed in the accompanying CSS changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Declare base64, bigdecimal, csv, and mutex_m explicitly activesupport, em-websocket, and tilt require these from the standard library but don't declare them, and Ruby 3.4 removes them from the default gems. Declaring them keeps the bundle working on 3.4+ and clears the deprecation warnings Ruby 3.3 prints on boot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Tighten the interstitial and fix the contribute heading overflowing on mobile Cut the block's dead vertical space (~30% shorter): the action row spans the full width so the two read buttons sit side by side instead of stacking, the contribute box padding drops from 4rem to --space-m, and the inter-block gaps tighten. Also fixes a mobile bug: the contribute heading had text-wrap: nowrap and spilled past the viewport on phones (horizontal scroll). It now falls back to the file's global text-wrap: balance — one line when it fits, graceful wrap otherwise. Drops the now-unused .missing-meta rule (its CTA is removed from the template). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Simplify and expand the interstitial copy Drop the "open a pull request or file an issue" line — the contribution-guide link in the body already covers it — removing the cta/pr_label/issue_label locale keys. Reword the contribute prompt to name the version explicitly ("version X"), using each language's own word for "version". Seed 11 more languages (Japanese, Korean, Dutch, Polish, Czech, Danish, Turkish, Ukrainian, Romanian, Indonesian, Arabic), bringing localized coverage to 22 of 28. The six most inflection-sensitive (Georgian, Farsi, Croatian, Slovenian, Serbian, Slovak) stay on the English fallback until a human provides them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5111b3c commit aa4413a

15 files changed

Lines changed: 2198 additions & 73 deletions

File tree

.github/workflows/css-lint.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CSS Lint
2+
3+
# Lints the hand-authored CSS with stylelint (see .stylelintrc.cjs) and fails the
4+
# build on any violation, so mistakes like duplicate selectors, redundant
5+
# longhands, or invalid values can't merge.
6+
7+
on:
8+
push:
9+
branches: [ "main", "2-0-0" ]
10+
pull_request:
11+
branches: [ "main" ]
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
stylelint:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Set up Node
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: '20'
26+
cache: 'npm'
27+
28+
- name: Install npm dependencies
29+
run: npm ci
30+
31+
- name: Lint CSS
32+
run: npm run lint:css

.stylelintrc.cjs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Stylelint config for the hand-authored CSS (source/assets/stylesheets/*.css).
2+
//
3+
// We extend stylelint-config-standard for its *correctness* rules — duplicate
4+
// selectors, redundant longhands, invalid values, empty blocks, and the like —
5+
// which catch real mistakes. We switch off the purely stylistic opinions that
6+
// fight this file's deliberate, documented house style, so the linter stays
7+
// signal (bugs) and not noise (reformatting). The .sass files use a different
8+
// syntax and are linted by neither this config nor the lint script.
9+
module.exports = {
10+
extends: ["stylelint-config-standard"],
11+
rules: {
12+
// Whitespace between rules/comments/declarations is authored intentionally.
13+
"comment-empty-line-before": null,
14+
"rule-empty-line-before": null,
15+
"declaration-empty-line-before": null,
16+
"custom-property-empty-line-before": null,
17+
"at-rule-empty-line-before": null,
18+
19+
// Compact single-line rules (e.g. `.foo { a: 1; b: 2; }`) are house style.
20+
"declaration-block-single-line-max-declarations": null,
21+
22+
// The palette is authored in decimal-lightness, unitless-hue oklch() on
23+
// purpose, and media queries use classic min-/max- rather than range syntax.
24+
"lightness-notation": null,
25+
"hue-degree-notation": null,
26+
"alpha-value-notation": null,
27+
"color-function-notation": null,
28+
"color-function-alias-notation": null,
29+
"media-feature-range-notation": null,
30+
31+
// Intentional names and values.
32+
"custom-property-pattern": null, // negative steps such as --step--1
33+
"value-keyword-case": null, // proper-case font-family names (Roboto, Menlo…)
34+
"property-no-vendor-prefix": null, // -webkit-mask etc. are needed for support
35+
36+
// Noisy against deliberately ordered, component-grouped rules; the real
37+
// cascade is verified visually, not by source order.
38+
"no-descending-specificity": null,
39+
},
40+
};

Gemfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ gem "redcarpet"
1414
gem "kramdown-parser-gfm"
1515
gem "standard", "~> 1.51"
1616

17-
group :development, :test do
17+
# activesupport, em-websocket, and tilt load these from the standard library but
18+
# don't declare them, and Ruby 3.4 drops them from the default gems. Declare them
19+
# so the bundle keeps working on 3.4+ (and Ruby stops warning on 3.3).
20+
gem "base64"
21+
gem "bigdecimal"
22+
gem "csv"
23+
gem "mutex_m"
24+
25+
group :development, :test do
1826
gem "minitest"
1927
end

Gemfile.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ GEM
1212
autoprefixer-rails (10.4.16.0)
1313
execjs (~> 2)
1414
backports (3.25.0)
15+
base64 (0.3.0)
16+
bigdecimal (4.1.2)
1517
coffee-script (2.4.1)
1618
coffee-script-source
1719
execjs
1820
coffee-script-source (1.12.2)
1921
concurrent-ruby (1.2.3)
2022
contracts (0.16.1)
23+
csv (3.3.5)
2124
dotenv (3.1.2)
2225
em-websocket (0.5.3)
2326
eventmachine (>= 0.12.9)
@@ -103,6 +106,7 @@ GEM
103106
middleman-core (>= 3.2)
104107
rouge (~> 3.2)
105108
minitest (5.25.2)
109+
mutex_m (0.3.0)
106110
padrino-helpers (0.15.3)
107111
i18n (>= 0.6.7, < 2)
108112
padrino-support (= 0.15.3)
@@ -181,6 +185,9 @@ PLATFORMS
181185

182186
DEPENDENCIES
183187
addressable
188+
base64
189+
bigdecimal
190+
csv
184191
kramdown-parser-gfm
185192
middleman (~> 4.5.1)
186193
middleman-autoprefixer
@@ -190,6 +197,7 @@ DEPENDENCIES
190197
middleman-minify-html
191198
middleman-syntax
192199
minitest
200+
mutex_m
193201
redcarpet
194202
standard (~> 1.51)
195203

config.rb

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@
164164
}
165165
$language_count = $languages.size
166166

167+
# Localized copy for the "translation missing" interstitial (source/missing.html.haml),
168+
# keyed by the same language codes as $languages. English is the canonical set and
169+
# the per-key fallback — see data/interstitial.yml and the interstitial_string helper.
170+
require "yaml"
171+
$interstitial = YAML.load_file("data/interstitial.yml")
172+
167173
activate :i18n,
168174
lang_map: $languages,
169175
mount_at_root: :en
@@ -183,6 +189,37 @@
183189
redirect "#{code}/index.html", to: "#{code}/#{$published_version_for.call(code)}/index.html"
184190
end
185191

192+
# Every (language, version) pair that hasn't been translated yet still resolves
193+
# to a real URL: a generated interstitial that explains the gap in the target
194+
# language (or English) and links to the English text plus how to contribute the
195+
# missing translation — instead of a 404. This is what lets the version/language
196+
# selectors offer *every* version for *every* language (issue #719): pick one we
197+
# don't have and you land somewhere helpful rather than nowhere.
198+
#
199+
# We mirror the selector's exposure rule: all spec versions when serving locally
200+
# (so an in-progress 2.0.0 draft previews), capped at the published $last_version
201+
# in a production build so unreleased drafts never ship.
202+
spec_versions = $versions.select { |v| File.directory?("source/en/#{v}") }
203+
204+
generate_interstitials = lambda do |max_version|
205+
$languages.each_key do |code|
206+
spec_versions.each do |version|
207+
next if max_version && Gem::Version.new(version) > Gem::Version.new(max_version)
208+
next if File.directory?("source/#{code}/#{version}") # real translation exists
209+
proxy "/#{code}/#{version}/index.html", "/missing.html",
210+
locals: { language_code: code, version: version }, ignore: true
211+
end
212+
end
213+
end
214+
215+
configure :development do
216+
generate_interstitials.call(nil)
217+
end
218+
219+
configure :build do
220+
generate_interstitials.call($last_version)
221+
end
222+
186223
# Patch releases of this project (e.g. 1.1.1) ship fixes to the site and tooling
187224
# without changing the changelog *specification*, so they never get their own
188225
# spec page — the spec stays at the x.y.0 minor (1.1.0). But those patch versions
@@ -237,9 +274,44 @@ def exposed_version_for(code)
237274
VersionRouting.exposed_version_for(installed, last_version: $last_version, build: build?)
238275
end
239276

240-
def available_translation_for(language)
241-
version = exposed_version_for(language.first)
242-
"#{version} #{language.last[:name]}" if version
277+
# The spec versions a visitor may pick from the version selector. We cap at the
278+
# published $last_version in a production build so unreleased drafts (e.g. an
279+
# in-progress 2.0.0) never surface; serving locally exposes the full set so
280+
# drafts can be previewed. Oldest → newest.
281+
def selectable_versions
282+
versions = spec_versions
283+
versions = versions.select { |v| Gem::Version.new(v) <= Gem::Version.new($last_version) } if build?
284+
versions
285+
end
286+
287+
# The real spec version directories ($versions also carries the en index
288+
# template that the glob picks up), oldest → newest.
289+
def spec_versions
290+
dirs = $versions.select { |v| File.directory?("source/en/#{v}") }
291+
dirs.sort_by { |v| Gem::Version.new(v) }
292+
end
293+
294+
# Whether a given spec version has actually been translated for a language.
295+
def version_available?(code, version)
296+
File.directory?("source/#{code}/#{version}")
297+
end
298+
299+
# major.minor for display (we never ship patch-level spec pages).
300+
def display_version(version)
301+
version.split(".").first(2).join(".")
302+
end
303+
304+
# A language's autonym (e.g. "Français"), falling back to its code.
305+
def language_name(code)
306+
($languages[code] || {})[:name] || code
307+
end
308+
309+
# A localized interstitial string for a language, keyed by e.g. "title" or
310+
# "lede". Falls back to English per key, so a partially-translated (or entirely
311+
# untranslated) language still renders a complete page. Interpolate the result
312+
# with String#% and the appropriate %{...} placeholders. See data/interstitial.yml.
313+
def interstitial_string(code, key)
314+
($interstitial[code] || {})[key] || $interstitial["en"][key]
243315
end
244316

245317
# The release date for a version, read from CHANGELOG.md (e.g. the line

0 commit comments

Comments
 (0)