jekyll-theme-resume 1.3.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.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/403.html +6 -0
  3. data/404.html +6 -0
  4. data/500.html +6 -0
  5. data/CHANGELOG.md +498 -0
  6. data/CODE_OF_CONDUCT.md +92 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +184 -0
  9. data/SECURITY.md +30 -0
  10. data/_config.sample.yml +329 -0
  11. data/_data/locales/ar.yml +85 -0
  12. data/_data/locales/de.yml +86 -0
  13. data/_data/locales/en.yml +84 -0
  14. data/_data/locales/es.yml +86 -0
  15. data/_data/locales/fr.yml +86 -0
  16. data/_data/locales/ur.yml +86 -0
  17. data/_data/social_networks.yml +83 -0
  18. data/_includes/analytics-body.html +12 -0
  19. data/_includes/analytics-head.html +36 -0
  20. data/_includes/avatar.html +84 -0
  21. data/_includes/dark-mode-toggle.html +202 -0
  22. data/_includes/data-loader.html +42 -0
  23. data/_includes/date-formatter.html +63 -0
  24. data/_includes/grouped-item-list.html +53 -0
  25. data/_includes/hreflang.html +58 -0
  26. data/_includes/language-switcher.html +72 -0
  27. data/_includes/print-social-links.html +23 -0
  28. data/_includes/resume-section.html +547 -0
  29. data/_includes/safe-url.html +21 -0
  30. data/_includes/shared-head.html +51 -0
  31. data/_includes/social-links.html +54 -0
  32. data/_includes/vendors/svg-icons/ATTRIBUTION.md +34 -0
  33. data/_includes/vendors/svg-icons/dev.svg +3 -0
  34. data/_includes/vendors/svg-icons/dribbble-symbol.svg +3 -0
  35. data/_includes/vendors/svg-icons/envelope.svg +4 -0
  36. data/_includes/vendors/svg-icons/facebook.svg +3 -0
  37. data/_includes/vendors/svg-icons/flickr.svg +4 -0
  38. data/_includes/vendors/svg-icons/github.svg +3 -0
  39. data/_includes/vendors/svg-icons/globe-1.svg +3 -0
  40. data/_includes/vendors/svg-icons/instagram.svg +4 -0
  41. data/_includes/vendors/svg-icons/linkedin.svg +3 -0
  42. data/_includes/vendors/svg-icons/medium.svg +3 -0
  43. data/_includes/vendors/svg-icons/phone.svg +13 -0
  44. data/_includes/vendors/svg-icons/pinterest.svg +3 -0
  45. data/_includes/vendors/svg-icons/postcard.svg +14 -0
  46. data/_includes/vendors/svg-icons/telegram.svg +3 -0
  47. data/_includes/vendors/svg-icons/whatsapp.svg +3 -0
  48. data/_includes/vendors/svg-icons/x.svg +3 -0
  49. data/_includes/vendors/svg-icons/youtube.svg +3 -0
  50. data/_layouts/default.html +65 -0
  51. data/_layouts/error.html +138 -0
  52. data/_layouts/profile.html +123 -0
  53. data/_layouts/resume.html +238 -0
  54. data/_plugins/error_pages_generator.rb +71 -0
  55. data/_plugins/json_resume_generator.rb +79 -0
  56. data/_plugins/resume_pages_generator.rb +71 -0
  57. data/_plugins/resume_validator.rb +40 -0
  58. data/_sass/_all-pages.scss +335 -0
  59. data/_sass/_base.scss +130 -0
  60. data/_sass/_dark-mode.scss +387 -0
  61. data/_sass/_layout.scss +116 -0
  62. data/_sass/_mixins.scss +136 -0
  63. data/_sass/_normalize.scss +379 -0
  64. data/_sass/_profile-page.scss +60 -0
  65. data/_sass/_resume-ltr.scss +436 -0
  66. data/_sass/_resume-rtl.scss +91 -0
  67. data/_sass/_variables.scss +45 -0
  68. data/assets/css/cv-ltr.scss +31 -0
  69. data/assets/css/cv-rtl.scss +36 -0
  70. data/assets/css/main.scss +8 -0
  71. data/assets/css/profile.scss +9 -0
  72. data/assets/favicon/resume/about.txt +6 -0
  73. data/assets/favicon/resume/android-chrome-192x192.png +0 -0
  74. data/assets/favicon/resume/android-chrome-512x512.png +0 -0
  75. data/assets/favicon/resume/apple-touch-icon.png +0 -0
  76. data/assets/favicon/resume/favicon-16x16.png +0 -0
  77. data/assets/favicon/resume/favicon-32x32.png +0 -0
  78. data/assets/favicon/resume/favicon.ico +0 -0
  79. data/assets/favicon/resume/site.webmanifest +1 -0
  80. data/bin/validate-resume +89 -0
  81. data/bin/verify +27 -0
  82. data/docs/README.md +89 -0
  83. data/docs/explanation/accessibility-decisions.md +27 -0
  84. data/docs/explanation/architecture.md +44 -0
  85. data/docs/explanation/dark-mode-approach.md +25 -0
  86. data/docs/explanation/data-driven-model.md +25 -0
  87. data/docs/explanation/multilingual-and-rtl-design.md +40 -0
  88. data/docs/how-to/add-a-language.md +57 -0
  89. data/docs/how-to/add-a-section.md +35 -0
  90. data/docs/how-to/add-a-social-platform.md +32 -0
  91. data/docs/how-to/add-a-test.md +34 -0
  92. data/docs/how-to/create-a-custom-layout.md +64 -0
  93. data/docs/how-to/enable-dark-mode.md +43 -0
  94. data/docs/how-to/link-translations-with-hreflang.md +25 -0
  95. data/docs/how-to/migrate-v0.9-to-v1.0.md +46 -0
  96. data/docs/how-to/override-locale-strings.md +51 -0
  97. data/docs/how-to/override-sass-partials.md +26 -0
  98. data/docs/how-to/proof-built-html.md +22 -0
  99. data/docs/how-to/publish-json-resume.md +38 -0
  100. data/docs/how-to/show-language-proficiency-in-header.md +13 -0
  101. data/docs/how-to/switch-resume-versions.md +42 -0
  102. data/docs/how-to/troubleshoot-builds.md +39 -0
  103. data/docs/how-to/validate-in-ci.md +38 -0
  104. data/docs/how-to/verify-accessibility.md +28 -0
  105. data/docs/reference/accessibility-coverage.md +39 -0
  106. data/docs/reference/config.md +378 -0
  107. data/docs/reference/data-schemas.md +529 -0
  108. data/docs/reference/glossary.md +39 -0
  109. data/docs/reference/includes.md +197 -0
  110. data/docs/reference/json-resume-fields.md +148 -0
  111. data/docs/reference/layouts.md +144 -0
  112. data/docs/reference/locale-keys.md +76 -0
  113. data/docs/reference/repository-map.md +83 -0
  114. data/docs/reference/sass-tokens.md +279 -0
  115. data/docs/reference/testing-suites.md +62 -0
  116. data/docs/reference/validator-cli.md +203 -0
  117. data/docs/tutorials/getting-started.md +173 -0
  118. data/lib/jekyll-theme-resume/json_resume_exporter.rb +334 -0
  119. data/lib/jekyll-theme-resume/resume_validator.rb +853 -0
  120. data/lib/jekyll-theme-resume/schemas/LICENSE.md +21 -0
  121. data/lib/jekyll-theme-resume/schemas/json_resume_v1.0.0.json +500 -0
  122. data/lib/jekyll-theme-resume.rb +21 -0
  123. metadata +371 -0
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require_relative "../lib/jekyll-theme-resume/resume_validator"
6
+
7
+ options = {
8
+ data_dir: nil,
9
+ config_path: nil,
10
+ languages: nil,
11
+ all_locales: false,
12
+ primary_locale: "en",
13
+ verbose: false,
14
+ quiet: false,
15
+ fail_on_warnings: false
16
+ }
17
+
18
+ parser = OptionParser.new do |opts|
19
+ opts.banner = "Usage: validate-resume [options] [DATA_DIR]"
20
+ opts.separator ""
21
+ opts.separator "Validates multilingual Jekyll resume YAML data for schema conformance, parity, and formatting."
22
+ opts.separator ""
23
+ opts.separator "Options:"
24
+
25
+ opts.on("-d", "--dir DIR", "Path to data directory (default: _data or demo/_data)") do |d|
26
+ options[:data_dir] = d
27
+ end
28
+
29
+ opts.on("-c", "--config FILE", "Jekyll config declaring `languages:` (default: _config.yml / _config.sample.yml next to DATA_DIR)") do |c|
30
+ options[:config_path] = c
31
+ end
32
+
33
+ opts.on("-l", "--languages LANGS", Array, "Comma-separated languages to validate (default: the config's `languages:`)") do |langs|
34
+ options[:languages] ||= []
35
+ options[:languages].concat(langs).uniq!
36
+ end
37
+
38
+ opts.on("-a", "--all-locales", "Also validate every language directory found in the data dir") do
39
+ options[:all_locales] = true
40
+ end
41
+
42
+ opts.on("-p", "--primary LOCALE", "Primary reference locale for parity checking (default: en)") do |p|
43
+ options[:primary_locale] = p
44
+ end
45
+
46
+ opts.on("-w", "--fail-on-warnings", "Treat warnings as errors and exit with code 1") do
47
+ options[:fail_on_warnings] = true
48
+ end
49
+
50
+ opts.on("-v", "--verbose", "Enable verbose validation output") do
51
+ options[:verbose] = true
52
+ end
53
+
54
+ opts.on("-q", "--quiet", "Suppress non-essential output (only report failures)") do
55
+ options[:quiet] = true
56
+ end
57
+
58
+ opts.on("-h", "--help", "Show this help message") do
59
+ puts opts
60
+ exit 0
61
+ end
62
+ end
63
+
64
+ parser.parse!
65
+
66
+ # Positional argument takes precedence over -d, or fall back to standard data dirs
67
+ data_dir = ARGV[0] || options[:data_dir]
68
+ if data_dir.nil?
69
+ # Prefer a directory that holds at least one language folder (a site's _data, else the theme's demo).
70
+ data_dir = %w[_data demo/_data].find do |dir|
71
+ JekyllThemeResume::ResumeValidator.new(dir).discover_languages.any?
72
+ end || "_data"
73
+ end
74
+
75
+ validator = JekyllThemeResume::ResumeValidator.new(
76
+ data_dir,
77
+ config_path: options[:config_path],
78
+ primary_locale: options[:primary_locale]
79
+ )
80
+ exit_code = validator.validate(
81
+ languages: options[:languages],
82
+ all_locales: options[:all_locales],
83
+ primary_locale: options[:primary_locale],
84
+ verbose: options[:verbose],
85
+ quiet: options[:quiet],
86
+ fail_on_warnings: options[:fail_on_warnings]
87
+ )
88
+
89
+ exit exit_code
data/bin/verify ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # Runs the AGENTS.md Rule 5 verification suite. Full output goes to a log;
3
+ # stdout gets one line per step plus the log tail of the first failure.
4
+ set -u
5
+ cd "$(dirname "$0")/.."
6
+ log="${TMPDIR:-/tmp}/resume-theme-verify.log"
7
+ : > "$log"
8
+
9
+ cleanup() { rm -rf .jekyll-cache jekyll-theme-resume-*.gem; }
10
+
11
+ step() {
12
+ local name=$1; shift
13
+ echo "=== $name: $*" >> "$log"
14
+ if "$@" >> "$log" 2>&1; then
15
+ echo "PASS $name"
16
+ else
17
+ echo "FAIL $name (full log: $log)"
18
+ tail -n 40 "$log"
19
+ cleanup; exit 1
20
+ fi
21
+ }
22
+
23
+ step build bundle exec jekyll build --source demo --destination _site
24
+ step rake bundle exec rake
25
+ step gem gem build jekyll-theme-resume.gemspec
26
+ cleanup
27
+ echo "ALL PASS (full log: $log)"
data/docs/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Documentation
2
+
3
+ This documentation is organized by what you need right now: learn the theme, do a task, look something up, or understand why it works the way it does. Each page states its audience under the title.
4
+
5
+ **Start here**
6
+
7
+ - New to the theme? Follow the [tutorial](tutorials/getting-started.md) to get a two-language resume running.
8
+ - Something broke? See [Troubleshoot builds](how-to/troubleshoot-builds.md).
9
+ - Not sure what a term means? See the [Glossary](reference/glossary.md).
10
+
11
+ ## Tutorial
12
+
13
+ | Page | Audience | You will |
14
+ |---|---|---|
15
+ | [Getting Started](tutorials/getting-started.md) | Site owners | Build and run an English and Arabic resume site from an empty folder. |
16
+
17
+ ## How-to guides
18
+
19
+ Steps for one task each.
20
+
21
+ | Page | Audience | Use it to |
22
+ |---|---|---|
23
+ | [Add a language](how-to/add-a-language.md) | Site owners | Add a new language with its locale file, data folder, and pages. |
24
+ | [Add a custom section](how-to/add-a-section.md) | Site owners and theme developers | Add a resume section that renders in every language. |
25
+ | [Add a social platform](how-to/add-a-social-platform.md) | Theme developers | Add a social network icon and label. |
26
+ | [Add a test](how-to/add-a-test.md) | Theme developers | Run the test suites and add a test for a change. |
27
+ | [Create a custom layout](how-to/create-a-custom-layout.md) | Theme developers | Add a page design that still renders in every language. |
28
+ | [Enable dark mode](how-to/enable-dark-mode.md) | Site owners and theme developers | Show the dark mode toggle site-wide or on one page. |
29
+ | [Link translations with hreflang](how-to/link-translations-with-hreflang.md) | Site owners | Connect the translations of a page for the language switcher and `hreflang` tags. |
30
+ | [Migrate from v0.9 to v1.0](how-to/migrate-v0.9-to-v1.0.md) | Site owners | Upgrade a v0.9.0 site. |
31
+ | [Override locale strings](how-to/override-locale-strings.md) | Site owners | Change UI text, fonts, error-page copy, or accepted "present" words. |
32
+ | [Override Sass partials](how-to/override-sass-partials.md) | Site owners | Change theme styles, including the accent color, without forking the gem. |
33
+ | [Proof the built HTML and lint the Ruby](how-to/proof-built-html.md) | Theme developers | Check a built site for dead links and missing assets, and run RuboCop. |
34
+ | [Publish the JSON Resume export](how-to/publish-json-resume.md) | Site owners | Serve the generated `resume.json` files. |
35
+ | [Show language proficiency in the header](how-to/show-language-proficiency-in-header.md) | Site owners | Show a one-line language summary in the resume header. |
36
+ | [Switch resume versions](how-to/switch-resume-versions.md) | Site owners | Load a different set of resume data for a language. |
37
+ | [Troubleshoot builds](how-to/troubleshoot-builds.md) | Site owners and theme developers | Diagnose a build problem from the symptom. |
38
+ | [Validate resume data in CI](how-to/validate-in-ci.md) | Site owners and theme developers | Gate builds on validation and run the checks in GitHub Actions. |
39
+ | [Verify accessibility](how-to/verify-accessibility.md) | Site owners and theme developers | Run the automated and manual accessibility checks. |
40
+
41
+ ## Reference
42
+
43
+ Facts to look up: keys, schemas, flags, and structure.
44
+
45
+ | Page | Audience | Contains |
46
+ |---|---|---|
47
+ | [Configuration reference](reference/config.md) | Site owners | Every `_config.yml` setting. |
48
+ | [Data schemas](reference/data-schemas.md) | Site owners | The YAML schema of every resume data file. |
49
+ | [Locale keys](reference/locale-keys.md) | Site owners and theme developers | The locale file schema, shipped locales, and override rules. |
50
+ | [JSON Resume export reference](reference/json-resume-fields.md) | Site owners | Export configuration, privacy, and field mappings. |
51
+ | [Validator and build checks](reference/validator-cli.md) | Site owners and theme developers | `validate-resume`, `check-data-keys`, rules by section, and the Ruby API. |
52
+ | [Accessibility coverage](reference/accessibility-coverage.md) | Site owners and theme developers | What the theme implements and what the automated checks cover. |
53
+ | [Layouts reference](reference/layouts.md) | Site owners and theme developers | The layouts, language resolution, and data loading. |
54
+ | [Includes reference](reference/includes.md) | Theme developers | Every include and its parameters. |
55
+ | [Sass reference](reference/sass-tokens.md) | Theme developers | The stylesheet architecture, partials, and dark mode tokens. |
56
+ | [Testing suites](reference/testing-suites.md) | Theme developers | What each test suite proves. |
57
+ | [Repository map](reference/repository-map.md) | Theme developers | The annotated code tree. |
58
+ | [Glossary](reference/glossary.md) | Site owners and theme developers | Definitions of terms used across the docs. |
59
+
60
+ ## Explanation
61
+
62
+ Why the theme is designed the way it is.
63
+
64
+ | Page | Audience |
65
+ |---|---|
66
+ | [Architecture](explanation/architecture.md) | Theme developers |
67
+ | [Multilingual and RTL design](explanation/multilingual-and-rtl-design.md) | Site owners and theme developers |
68
+ | [The data-driven model](explanation/data-driven-model.md) | Site owners and theme developers |
69
+ | [Dark mode approach](explanation/dark-mode-approach.md) | Site owners and theme developers |
70
+ | [Accessibility decisions](explanation/accessibility-decisions.md) | Site owners and theme developers |
71
+
72
+ ## Working on the theme: where things live and how to check a change
73
+
74
+ These commands assume a checkout of the theme repository.
75
+
76
+ | Need / Task | Go to | Check your change |
77
+ |---|---|---|
78
+ | Configure site settings, languages, avatar, or analytics | `_config.yml`, [Configuration reference](reference/config.md) | Run the demo build; see [Getting Started](tutorials/getting-started.md) |
79
+ | Add a language or change UI strings, fonts, or month names | `_data/locales/<lang>.yml`, [Add a language](how-to/add-a-language.md), [Override locale strings](how-to/override-locale-strings.md), [Locale keys](reference/locale-keys.md) | `./bin/validate-resume demo/_data`, then inspect `_site/<lang>/cv/` |
80
+ | Modify resume section data | `_data/<lang>/*.yml`, [Data schemas](reference/data-schemas.md) | `./bin/validate-resume demo/_data` |
81
+ | Customize error pages or return URLs | `_layouts/error.html`, `_plugins/error_pages_generator.rb`, `error_pages` in the locale files, [Layouts reference](reference/layouts.md), [Locale keys](reference/locale-keys.md) | Inspect `_site/404.html`, `_site/500.html` |
82
+ | Adjust dark mode colors or tokens | `_sass/_dark-mode.scss`, [Sass reference](reference/sass-tokens.md), [Dark mode approach](explanation/dark-mode-approach.md) | Inspect CSS variables on `:root` and `[data-theme="dark"]` |
83
+ | Adjust RTL mirroring | `_sass/_resume-rtl.scss`, [Sass reference](reference/sass-tokens.md) | Inspect `_site/ar/cv/` and `_site/ur/cv/` |
84
+
85
+ ## Project files
86
+
87
+ - [Changelog](../CHANGELOG.md): release notes.
88
+ - [`_config.sample.yml`](../_config.sample.yml): the annotated sample configuration for a consuming site.
89
+ - [Contribution and agent rules](https://github.com/kmutahar/jekyll-theme-resume/blob/master/AGENTS.md) (`AGENTS.md`, not shipped in the gem).
@@ -0,0 +1,27 @@
1
+ # Accessibility decisions
2
+
3
+ *Audience: site owners and theme developers*
4
+
5
+ This page explains the accessibility choices the theme makes, and the limits that come with them. For what is covered and how to check it, see [accessibility coverage](../reference/accessibility-coverage.md) and [verify accessibility](../how-to/verify-accessibility.md).
6
+
7
+ ## What the theme claims, and what it does not
8
+
9
+ The theme describes its accessibility features and the checks a consuming site needs; it is not a certification of complete WCAG conformance. That is deliberate. Content, custom colors, third-party integrations, and browser behavior all affect the result, and none of them are under the theme's control. A theme can supply sound building blocks, but only the finished site can be judged.
10
+
11
+ ## Structure and keyboard navigation
12
+
13
+ Headers and footers use semantic HTML, and the resume also supplies explicit banner and contentinfo roles. The profile header sits outside its main content, so its skip link bypasses the name and bio. That is a trade-off of the layout: a keyboard user jumps past the header block, including the name and bio, to reach the content.
14
+
15
+ `_sass/_base.scss` owns `.sr-only`, the skip-link styles, and the `:focus-visible` outlines, and the skip link becomes visible on focus. The language switcher (`_includes/language-switcher.html`) uses native `<details>`/`<summary>` and a labelled `<nav>`. Enter and Space toggle the disclosure. The cost of choosing the native element is that Escape and outside clicks do not automatically close it, because the theme adds no script to imitate that behavior.
16
+
17
+ The language switcher is fixed top-left, and the optional theme toggle top-right, in every language, and both are hidden for print. Fixed positioning keeps them in the same place across languages, but small-screen overlap and focus visibility still need browser testing.
18
+
19
+ ## Color contrast
20
+
21
+ Contrast is a property of a foreground/background pair, not of a token in isolation, so the theme's tokens have to be judged in the pairs they are used in. For example, the muted text token on a white background falls short of the AA contrast ratio for normal-size text; the measured ratios are in [accessibility coverage](../reference/accessibility-coverage.md#color-contrast). The token values are listed in [SASS tokens](../reference/sass-tokens.md).
22
+
23
+ Hover, focus, disabled, print, and customized colors each need to be checked separately from the default state. Focus outlines do not establish that pointer targets meet target-size requirements. Hiding the theme toggle does not force a light palette: system and stored preferences still apply (see [the dark mode approach](dark-mode-approach.md)).
24
+
25
+ ## Summary
26
+
27
+ The theme ships `.sr-only` utilities, visible `:focus-visible` outlines, landmarks, and localized skip links. Known limits are the ones above; coverage and verification steps are in the reference and how-to pages linked at the top.
@@ -0,0 +1,44 @@
1
+ # Architecture
2
+
3
+ *Audience: theme developers*
4
+
5
+ This page explains how the theme fits together: the layouts and generators, how the pieces are wired, the two verification engines, and the Liquid pitfalls that shape the templates. For the annotated file tree see [repository map](../reference/repository-map.md); to build the demo site, see [getting started](../tutorials/getting-started.md).
6
+
7
+ **jekyll-theme-resume** is a Ruby gem / Jekyll theme for data-driven, multilingual resume and CV sites. One locale-agnostic layout renders every language, left-to-right or right-to-left, from YAML data and per-language locale files. The theme ships six locales: English, Arabic, Spanish, French, German, and Urdu.
8
+
9
+ ---
10
+
11
+ ## Components
12
+
13
+ ### Layouts and generators
14
+ The four layouts (`default.html`, `profile.html`, `resume.html`, `error.html`) are described in the [layouts reference](../reference/layouts.md#layout-inventory).
15
+
16
+ - `_plugins/error_pages_generator.rb` adds missing `404`, `403`, and `500` pages to consuming sites. A manual error page can use `code: 503`.
17
+ - `_plugins/resume_pages_generator.rb` creates missing CV pages at each `languages.<lang>.url` and profiles at `/` or `/<lang>/`. Manual pages take precedence; see [page configuration](../reference/config.md#3-languages).
18
+
19
+ ### Two verification engines
20
+
21
+ The repository has two verification engines, and only the resume-data validator ships in the gem: it checks resume YAML data, including during every consuming site's build. The template key checker checks the theme's own templates and runs only in this repository's development workflow and CI, never on a consuming site's build. The entry points and what each one checks are in the [validator reference](../reference/validator-cli.md).
22
+
23
+ `check-data-keys` catches a different class of bug than the resume validator: not bad *data*, but a Liquid template in `_layouts` or `_includes` referencing a field that doesn't exist anywhere in the checked sample data (e.g. `item.discription` instead of `item.description`), which renders silently blank rather than raising an error. It traces `resume_data.<section>` bindings through `for`/`assign`/include-parameter chains, including the `grouped-item-list.html` include boundary and the `group_by` filter's synthetic `{name, items}` wrapper, since the theme's templates never write literal `site.data.foo.bar`.
24
+
25
+ Because "known keys" are derived from whatever the checked sample data actually contains, a field a template correctly references but that no language in the checked data happens to exercise (an optional field, e.g. `education.yml`'s `awards` list) will warn even though nothing is wrong. For this reason `check-data-keys` never defaults to `--fail-on-warnings` in this repository's Rake task or CI steps, unlike `validate-resume`. Warnings are printed and worth reading, but a warning alone does not mean the template is broken; cross-check against the field before "fixing" it.
26
+
27
+ ### Accessibility
28
+ - `.sr-only` utilities, visible `:focus-visible` outlines, landmarks, and localized skip links. Coverage and known contrast limitations are documented in [accessibility coverage](../reference/accessibility-coverage.md).
29
+
30
+ ### Liquid pitfalls
31
+
32
+ Three Liquid behaviors shape how the templates are written, and the tests guard each of them.
33
+
34
+ - **Presence checks never use `blank`.** Liquid's `blank` literal calls Ruby's `blank?`, which plain Jekyll strings and `nil` do not define, so `x != blank` is always true and would render empty sections. The templates use `x.size > 0` for text and a plain truthiness test `x` for dates.
35
+ - **Dates are never passed to the `date` filter.** The filter reads a bare year such as `2018` as a Unix timestamp. `date-formatter.html` therefore parses the ISO parts itself.
36
+ - **Includes share the caller's variables.** An include has no scope of its own, so an unprefixed loop variable would overwrite `lang`, `locale`, or `lang_cfg` in `resume.html`. Loop and helper variables inside includes carry a prefix (`switch_` in `language-switcher.html`, `social_` in the social includes).
37
+
38
+ ### Why `@use` instead of `@import`
39
+
40
+ The theme uses modern Dart Sass `@use` exclusively, instead of the deprecated `@import`. Namespacing keeps variables and mixins encapsulated (for example `variables.$white`, `@include mixins.clearfix`), which prevents global pollution. Modules load only what they explicitly require.
41
+
42
+ ### Tests use public interfaces only
43
+
44
+ Every test goes through a public interface: the HTML a visitor receives, a Ruby class's public methods, a CLI's exit code and output, or the gem's file list. No test calls a private method. Suites are listed in [testing suites](../reference/testing-suites.md).
@@ -0,0 +1,25 @@
1
+ # Dark mode approach
2
+
3
+ *Audience: site owners and theme developers*
4
+
5
+ This page explains why dark mode is built the way it is: every color is a token, activation has two tiers, and a small inline script prevents a flash. For the token values see [SASS tokens](../reference/sass-tokens.md); to turn the toggle on, see [enable dark mode](../how-to/enable-dark-mode.md).
6
+
7
+ ## One place for every color
8
+
9
+ [`_sass/_dark-mode.scss`](../../_sass/_dark-mode.scss) holds every color token on `:root`. Keeping the palette in tokens means a dark palette is a matter of redefining variables rather than restyling components.
10
+
11
+ ## Two tiers of activation
12
+
13
+ Activation has two tiers. The first is automatic: the stylesheet uses `prefers-color-scheme: dark`, applied to `:root` unless it is explicitly pinned to light through `data-color-scheme="light"` or `data-theme="light"`. So `dark_mode: auto` adapts to the visitor's system with no JavaScript. The second is an explicit pin: `:root[data-color-scheme="dark"]` or `:root[data-theme="dark"]` sets the dark palette regardless of the system. `dark_mode: enabled` adds a toggle that uses `localStorage` to persist the visitor's choice. The `:not(...)` guard on the first tier is what lets an explicit light pin win over a dark system preference. The first tier is a no-JavaScript baseline that respects the operating system; the second exists so a visitor can override it.
14
+
15
+ ## Avoiding a flash of the wrong theme
16
+
17
+ A stored preference lives in `localStorage`, which only script can read, and stylesheets would otherwise paint first. To prevent theme flashing (FOUC), an inline `<head>` script in [`_includes/shared-head.html`](../../_includes/shared-head.html) applies the stored preference before stylesheets load.
18
+
19
+ ## What the settings do not do
20
+
21
+ Page front matter `dark_mode: false` or `true` controls the toggle on one page. These settings do not disable the system-preference CSS or the stored-preference script; `false` hides the button rather than forcing a light palette. Hiding the control and removing the behavior are different things: a visitor whose system is dark, or who stored a preference earlier, still gets that palette. See [config](../reference/config.md) for the setting itself.
22
+
23
+ ## Print
24
+
25
+ Dark mode does not apply to print or PDF. `@media print` in `_sass/_dark-mode.scss` forces black text on white, and the toggle carries `.no-print`, so the control does not appear on paper either.
@@ -0,0 +1,25 @@
1
+ # The data-driven model
2
+
3
+ *Audience: site owners and theme developers*
4
+
5
+ This page explains why resume content lives in per-language YAML data, how the theme finds and reads it, and the limits of the JSON export. For schemas see [data schemas](../reference/data-schemas.md), and for the lookup mechanics see [layouts](../reference/layouts.md).
6
+
7
+ ## Content as data, one folder per language
8
+
9
+ Resume content lives in YAML files, one folder per language. Keeping one folder per language (`_data/en/`, `_data/ar/`, `_data/es/`, and so on), each holding the same file names, means every language renders through the same layout and only the data differs. Complete starter data for six languages (a Sherlock Holmes demo persona) is in [`demo/_data/`](../../demo/_data/): `en`, `ar`, `es`, `fr`, `de`, `ur`.
10
+
11
+ Each language's folder is set by `languages.<lang>.data_path` in `_config.yml`. Dot paths such as `"2025-06.v1"` select nested, versioned datasets, so several versions of a resume can live side by side and a language can point at whichever one it needs. See [config](../reference/config.md) for the setting.
12
+
13
+ ## Why lookups are written the way they are
14
+
15
+ Sections render in the order of `site.resume_section_order` through one dispatcher, [`_includes/resume-section.html`](../../_includes/resume-section.html), plus the `header.yml` intro. Order is therefore configuration, not template code; the list of sections is in [data schemas](../reference/data-schemas.md).
16
+
17
+ Two Liquid details shape how data is read. Presence checks use `field.size > 0` for text or a plain truthiness test for dates, never `!= blank`; the reason is in [Liquid pitfalls](architecture.md#liquid-pitfalls).
18
+
19
+ Bracket access (`resume_data[part]`) is what makes folder names like `2025-06` or `20250621-PM` work; Liquid dot notation (`site.data.2025-06`) fails on leading digits and hyphens. Using brackets is what allows dated, versioned folder names.
20
+
21
+ ## Privacy and the limits of the JSON export
22
+
23
+ The JSON export is built from the same data, but it is a supported subset of the CV, not a lossless representation of every website field. Documented optional enrichment fields can appear in JSON even if HTML does not display those fields.
24
+
25
+ `json_resume.privacy.export_contact_info: false` omits the structured contact fields; the exact list is in [Visibility and privacy](../reference/json-resume-fields.md#visibility-and-privacy). The setting has limits: it does not redact free-form narrative text, usernames, or arbitrary URLs, and it does not change the HTML site's contact visibility. Treat it as a switch for the structured contact fields, not as a general privacy filter. Field details are in [JSON Resume fields](../reference/json-resume-fields.md).
@@ -0,0 +1,40 @@
1
+ # Multilingual and RTL design
2
+
3
+ *Audience: site owners and theme developers*
4
+
5
+ Why the theme treats a language as data rather than code, how theme and site locale files layer, and why right-to-left support is language-neutral. For task steps see [add a language](../how-to/add-a-language.md) and [override locale strings](../how-to/override-locale-strings.md); for the key list see the [locale keys reference](../reference/locale-keys.md).
6
+
7
+ ## What a language is
8
+
9
+ Since v1.0.0 the theme renders every language through one layout, `_layouts/resume.html`. A language is three things:
10
+
11
+ 1. A **locale file**, `_data/locales/<lang>.yml`: text direction, font, line height, UI strings, month names, "present" words, and error page copy.
12
+ 2. A **data folder**, `_data/<data_path>/`: the resume content (`experience.yml`, `education.yml`, and so on). Schemas are in [`data-schemas.md`](../reference/data-schemas.md).
13
+ 3. A **config entry**, `languages.<lang>` in `_config.yml`: data path, URL, name, title, address, avatar alt text, and the header intro toggle.
14
+
15
+ Because nothing else distinguishes one language from another, a language the theme does not ship is added by the site alone, with no Ruby, HTML, or SCSS changes. The shipped locales are listed in the [locale keys reference](../reference/locale-keys.md#shipped-locales). Pages only set `layout: resume` and `lang: <code>`; the layout reads direction and UI copy from the active locale and never branches on a specific language code.
16
+
17
+ ## One date formatter for every language
18
+
19
+ Dates in the data are written as ISO values and month names come from the locale file, so the data stays language-neutral. [`_includes/date-formatter.html`](../../_includes/date-formatter.html) localizes month names and "Present" for every language. It splits the ISO value itself rather than using Liquid's `date` filter, which reads a bare `2018` as a Unix timestamp. This include is the single date hook for every language: calendar extensions (for example Hijri dates) belong here rather than in a language-specific include. The accepted "present" words are described in the [locale keys reference](../reference/locale-keys.md#present-values).
20
+
21
+ ---
22
+
23
+ ## Overriding Theme Locales
24
+
25
+ The six locale files ship inside the theme gem, so consuming sites get them with no setup. Jekyll reads theme data first and then deep-merges the site's `_data/` over it; the site wins. This layering is what lets a site change one heading with a one-key file, or replace a whole locale, without forking the theme or waiting for a gem release.
26
+
27
+ The validator mirrors the same layering: it builds each language's effective locale from the theme file with the site file deep-merged over it, and checks key parity on that merged result. Checking the merged result rather than the site file alone is why a one-line site override warns on nothing, while a site-only locale for a language the theme does not ship must be complete. The merge rules (including how arrays behave) are in the [locale keys reference](../reference/locale-keys.md#overriding-theme-locales).
28
+
29
+ ---
30
+
31
+ ## Typography & RTL
32
+
33
+ - **Fonts and line height come only from the locale file.** The layout emits them as CSS variables and the stylesheet reads them with the theme's default stacks as fallbacks, so changing a language's font is a locale override, not an SCSS edit. The shipped line heights follow the script: Arabic uses Cairo at `1.6` (room for diacritics); Urdu uses Noto Nastaliq Urdu at `2.0` (Nastaliq glyphs are tall); the LTR locales use the default stacks at `1.5`. The variable names are in the [Sass tokens reference](../reference/sass-tokens.md).
34
+ - **Direction selects the stylesheet.** The layout links `cv-<direction>.css`, where `direction` comes from the locale file, and sets `<html dir>` from the same value.
35
+ - **RTL is language-neutral.** Every RTL locale compiles through `assets/css/cv-rtl.scss`, which loads `_sass/_resume-ltr.scss` and then `_sass/_resume-rtl.scss`. The RTL partial only mirrors positioning (floats, margins, timeline bullets, contact icons; floated elements such as the avatar, social bar, and job title flip alignment) under `html[dir="rtl"]` and resets `letter-spacing` so cursive scripts keep their ligatures. It sets no fonts, so Arabic, Urdu, and any future RTL language each keep their own typeface.
36
+ - **Bidi isolation.** In RTL locales, templates wrap phone numbers, email addresses, URLs, and credential IDs in `dir="ltr"` so Latin punctuation is not reordered.
37
+
38
+ ---
39
+
40
+ Upgrading from the v0.9.0 per-language layouts (`resume-en`, `resume-ar`) to this model: see [migrate from v0.9 to v1.0](../how-to/migrate-v0.9-to-v1.0.md).
@@ -0,0 +1,57 @@
1
+ # Add a language
2
+
3
+ *Audience: site owners*
4
+
5
+ Add a new language (Italian, `it`, in this example) to your site, and optionally hand-author its page. The theme has no `it.yml`, so the site supplies a complete locale file.
6
+
7
+ ## Steps
8
+
9
+ 1. **Create the locale file.** Copy the theme's [`_data/locales/en.yml`](../../_data/locales/en.yml) (find the installed theme with `bundle info --path jekyll-theme-resume`; the shipped locales are listed in [Locale keys](../reference/locale-keys.md#shipped-locales)) to your site as `_data/locales/it.yml`, keep every key, and translate the values. Set `direction`, `font_family`, `font_url`, and `line_height` for the script (see [multilingual and RTL design](../explanation/multilingual-and-rtl-design.md)). Replace `months` with the 12 Italian month names and `present_values` with the words your data uses for ongoing roles (for example `["present", "presente", "attuale"]`). Keep `present` in `present_values`: Experience and Volunteering fall back to the English word "Present" for a blank `enddate`, and the formatter translates it only when `present_values` contains it.
10
+ 2. **Create the data folder.** Copy an existing language folder (for example `_data/en/`) to `_data/it/` and translate every file. Keep the same file names and the same entries in the same order; the validator reports files that exist in one language and not the other.
11
+ 3. **Register the language** in `_config.yml`:
12
+ ```yaml
13
+ languages:
14
+ it:
15
+ data_path: it # folder under _data/; dot paths like "2025-06.it" also work
16
+ url: /it/cv/ # used by error pages, hreflang, and the language switcher
17
+ header_intro: true # render _data/it/header.yml intro under the header
18
+ name: "Nome Cognome"
19
+ resume_title: "Titolo professionale"
20
+ address: "Città, Paese"
21
+ avatar_alt: "Foto di Nome Cognome"
22
+ ```
23
+ 4. **Page (optional).** With `resume_auto_generate_pages` at its default `true`, the theme auto-generates the CV page at `languages.it.url` and a profile page at `/it/` for you, both carrying `t_id: resume` / `t_id: profile`. Create your own `it/cv.md` (`layout: resume`, `lang: it`, `permalink` matching `languages.it.url`) only if you want to hand-author it instead - a hand-authored page always takes precedence. See `resume_auto_generate_pages` / `languages.<lang>.auto_generate_pages` in the [config reference](../reference/config.md).
24
+ 5. **Validate and build:**
25
+ ```bash
26
+ bundle exec validate-resume _data
27
+ bundle exec jekyll build
28
+ ```
29
+ Done when the validator reports no errors for `it` and `_site/it/cv/index.html` renders with Italian section titles and month names.
30
+
31
+ The error pages and the language switcher pick up the new language automatically because they loop over `site.languages`. hreflang tags pick it up when the new page shares a `t_id` with its translations: see [Link translations with hreflang](link-translations-with-hreflang.md).
32
+
33
+ ## Hand-author a page
34
+
35
+ To override a generated page, or disable generation with `resume_auto_generate_pages: false`, create a page with matching `layout` and `lang`:
36
+
37
+ ```markdown
38
+ ---
39
+ layout: resume
40
+ lang: it
41
+ permalink: /it/cv/
42
+ t_id: resume # optional: links translations for hreflang and the language switcher
43
+ ---
44
+ ```
45
+
46
+ How the layout resolves a page's language is described in [Language Resolution](../reference/layouts.md#language-resolution).
47
+
48
+ ## Content conventions
49
+
50
+ - Translate every data file natively; copying English into another language's folder produces a resume that looks localized in the headings and English in the body.
51
+ - Keep proper nouns consistent: transliterate them in non-Latin scripts and keep them as-is in Latin-script languages.
52
+ - Write dates as ISO (`YYYY-MM-DD`, `YYYY-MM`, or `YYYY`). Month names come from the locale file, so the data stays language-neutral.
53
+ - For ongoing roles, leave `enddate` blank or use any value from that locale's `present_values`.
54
+
55
+ ## Troubleshoot: a page renders with no name or content
56
+
57
+ The page's `lang` has no entry under `languages:`, or that entry's `data_path` points at a folder that does not exist. Run `bundle exec validate-resume _data`.
@@ -0,0 +1,35 @@
1
+ # Add a custom section
2
+
3
+ *Audience: site owners and theme developers*
4
+
5
+ Add a new resume section (`publications` in this example) that renders in every language.
6
+
7
+ ## Steps
8
+
9
+ 1. **Add the heading to every locale.** Add `ui.section_titles.publications` to each `_data/locales/<lang>.yml` you ship (in a consuming site, add it through site locale overrides; nested keys merge, so a one-key file per language is enough).
10
+ 2. **Add one branch** to `_includes/resume-section.html` (in a consuming site, first copy the theme's file to your own `_includes/resume-section.html`), just before the final `{% endif %}`. It serves every language:
11
+ ```liquid
12
+ {% elsif include.section_name == "publications" and site.resume_section.publications %}
13
+ <section class="content-section">
14
+ <header class="section-header">
15
+ <h2>{{ locale.ui.section_titles.publications }}</h2>
16
+ </header>
17
+ {% for item in resume_data.publications %}
18
+ {% if item.active == true %}
19
+ <div class="resume-item">
20
+ <h3 class="resume-item-title">{{ item.title }}</h3>
21
+ <p class="resume-item-details">{{ item.publisher }} &bull; {{ item.year }}</p>
22
+ </div>
23
+ {% endif %}
24
+ {% endfor %}
25
+ </section>
26
+ ```
27
+ 3. **Add data** as `publications.yml` in every language folder. Each file must be a list of items, each with an `active` flag; the validator checks only that shape for custom sections.
28
+ 4. **Enable it:** `resume_section.publications: true` and `- publications` in `resume_section_order`.
29
+
30
+ ## See also
31
+
32
+ - [Show language proficiency in the header](show-language-proficiency-in-header.md)
33
+ - [Includes reference](../reference/includes.md)
34
+ - [Data schemas](../reference/data-schemas.md)
35
+ - [Config reference](../reference/config.md)
@@ -0,0 +1,32 @@
1
+ # Add a social platform
2
+
3
+ *Audience: theme developers*
4
+
5
+ Add a new social network icon and label to the theme.
6
+
7
+ ## Steps
8
+
9
+ 1. Place an optimized SVG in `_includes/vendors/svg-icons/newplatform.svg`. Match the shipped icons: a square image with `width="24" height="24" viewBox="0 0 24 24"`, and `aria-hidden="true"` and `focusable="false"` on the `<svg>` element, because the link around the icon carries the accessible name. The stylesheet sets the icon color (`.icon-link svg`), so any fill colors inside the SVG do not matter. If the icon comes from a licensed set, add a row to the table in that folder's `ATTRIBUTION.md` giving the file name and the set it came from.
10
+ 2. Add an entry to [`_data/social_networks.yml`](../../_data/social_networks.yml), the single list both `social-links.html` and `print-social-links.html` loop over. The order in this file is the order of the icons in the header and of the print list:
11
+ ```yaml
12
+ - key: newplatform
13
+ icon: newplatform
14
+ itemprop: sameAs # or "url" for a non-profile link
15
+ label: New Platform
16
+ ```
17
+ `key` is the name a site uses under `social_links:` in `_config.yml`. `icon` is the SVG file name without `.svg`. Use `itemprop: sameAs` for a profile that verifies who the person is, `url` for a website or other link, and `email` for an email address. `label` is the English fallback for the icon's accessible name; each locale's label wins when it has one.
18
+ 3. Add a `ui.social_labels.newplatform` key to every locale file (the icon's accessible name and the print-only label; the print list has no fallback):
19
+ ```yaml
20
+ # _data/locales/<lang>.yml, in each of the six shipped locales
21
+ ui:
22
+ social_labels:
23
+ newplatform: "New Platform" # translate for each language
24
+ ```
25
+ Once the entry from step 2 exists, `test/test_packaging.rb` fails until its SVG and all six labels exist.
26
+ 4. To include the platform in the JSON Resume export, add its `key` to `JsonResumeExporter::NETWORKS` in [`lib/jekyll-theme-resume/json_resume_exporter.rb`](../../lib/jekyll-theme-resume/json_resume_exporter.rb). That list is hard-coded, so a platform missing from it is never exported.
27
+
28
+ ## Check it
29
+
30
+ 1. Run `bundle exec ruby test/test_packaging.rb`. It fails with the missing icon's file name, or with the language and key of a missing label, until steps 1 to 3 are complete.
31
+ 2. Add the platform to `social_links:` in a site's `_config.yml` (for example `newplatform: "https://example.com/you"`), build, and look for the icon in the header's social row. Hovering over it shows the label.
32
+ 3. Open the print preview. The print-only social list shows the platform's label followed by its address.
@@ -0,0 +1,34 @@
1
+ # Add a test
2
+
3
+ *Audience: theme developers*
4
+
5
+ Run the test suites and add a new test for a change.
6
+
7
+ ## Run the tests
8
+
9
+ ```bash
10
+ git submodule update --init --recursive # demo/ data is used by several suites
11
+ bundle exec rake # validate + check_data_keys + rubocop + every test/test_*.rb
12
+ bundle exec rake test # tests only
13
+ bundle exec ruby test/test_rendered_site.rb # one suite
14
+ bundle exec ruby test/test_rendered_site.rb -n /date/ # tests whose name matches
15
+ ```
16
+
17
+ ## Add a test
18
+
19
+ 1. Pick the interface a user or consuming site touches: rendered HTML for templates, `ResumeValidator#validate` for data rules, a site build for generators.
20
+ 2. Write the failing test first and run it to see the failure message.
21
+ 3. Make the smallest change that passes it, then run `bundle exec rake`.
22
+
23
+ ## Common cases
24
+
25
+ - **New section field:** add it to `resume_data` in `test_rendered_site.rb` and assert on the rendered text; add a validator rule test if the field is validated.
26
+ - **New locale key:** add it to all six `_data/locales/*.yml` files. `test_packaging.rb` fails if the key sets differ or no template reads the key.
27
+ - **New social platform:** follow [Add a social platform](add-a-social-platform.md); `test_packaging.rb` checks the SVG and labels.
28
+ - **New generator:** require it from `lib/jekyll-theme-resume.rb` and add its class to the registration test in `test_error_pages_generator.rb`.
29
+
30
+ ## See also
31
+
32
+ - [Testing suites reference](../reference/testing-suites.md)
33
+ - [Validator CLI](../reference/validator-cli.md)
34
+ - [Verification rules](../../AGENTS.md#rule-5-build--packaging-verification)
@@ -0,0 +1,64 @@
1
+ # Create a custom layout
2
+
3
+ *Audience: theme developers*
4
+
5
+ Add a new page design (for example `_layouts/academic-cv.html`) that still renders in every language. A new language never needs a new layout; see [Add a language](add-a-language.md).
6
+
7
+ ## Steps
8
+
9
+ 1. Create `_layouts/academic-cv.html` in your site. Start from the skeleton below, or copy the theme's `resume.html` for a full resume variant.
10
+ 2. Resolve `lang`, `locale`, and `lang_cfg` with the three lines shown under Language Resolution in the [layouts reference](../reference/layouts.md), and read every visible string from `locale.ui` so the layout works in every language.
11
+ 3. Load data with `{% include data-loader.html path=lang_cfg.data_path %}`.
12
+ 4. Reuse the shared includes ([`shared-head.html`](../../_includes/shared-head.html), [`avatar.html`](../../_includes/avatar.html), [`dark-mode-toggle.html`](../../_includes/dark-mode-toggle.html), [`resume-section.html`](../../_includes/resume-section.html)).
13
+ 5. Reference the layout from a page:
14
+ ```yaml
15
+ ---
16
+ layout: academic-cv
17
+ lang: en
18
+ ---
19
+ ```
20
+
21
+ ## A working skeleton
22
+
23
+ This layout renders the resume sections in any configured language:
24
+
25
+ ```liquid
26
+ {%- assign lang = page.lang | default: site.default_lang | default: 'en' -%}
27
+ {%- assign locale = site.data.locales[lang] | default: site.data.locales[site.default_lang] -%}
28
+ {%- assign lang_cfg = site.languages[lang] -%}
29
+ {% include data-loader.html path=lang_cfg.data_path %}
30
+ <!DOCTYPE html>
31
+ <html lang="{{ lang }}" dir="{{ locale.direction }}">
32
+ <head>
33
+ {% include shared-head.html %}
34
+ <link rel="stylesheet" href="{{ 'assets/css/cv-' | append: locale.direction | append: '.css' | relative_url }}">
35
+ {% seo %}
36
+ </head>
37
+ <body>
38
+ <a href="#main-content" class="skip-link no-print">{{ locale.ui.skip_to_content }}</a>
39
+ {% include dark-mode-toggle.html %}
40
+ {% include language-switcher.html %}
41
+ <main id="main-content" role="main" tabindex="-1">
42
+ <h1>{{ lang_cfg.name }}</h1>
43
+ {%- for section_name in site.resume_section_order -%}
44
+ {% include resume-section.html section_name=section_name lang=lang %}
45
+ {%- endfor -%}
46
+ </main>
47
+ </body>
48
+ </html>
49
+ ```
50
+
51
+ What each part does:
52
+
53
+ - The first four lines resolve the language and load that language's data into `resume_data`.
54
+ - `dir="{{ locale.direction }}"` and the stylesheet name come from the locale, so one layout serves left-to-right and right-to-left languages: the page links `cv-ltr.css` or `cv-rtl.css`.
55
+ - `shared-head.html` adds the charset, viewport, favicons, and the script that applies a saved dark-mode choice before the page paints.
56
+ - The loop renders each section listed in `resume_section_order` that is switched on under `resume_section:` in `_config.yml`.
57
+
58
+ ## Check it
59
+
60
+ 1. Create one page per language, for example `academic.md` (`layout: academic-cv`, `lang: en`, `permalink: /academic/`) and `academic-ar.md` (`lang: ar`, `permalink: /ar/academic/`).
61
+ 2. Build the site with `bundle exec jekyll build`.
62
+ 3. Open `_site/academic/index.html` and `_site/ar/academic/index.html`. The English page starts with `<html lang="en" dir="ltr">` and links `cv-ltr.css`. The Arabic page starts with `<html lang="ar" dir="rtl">`, links `cv-rtl.css`, and its section headings are in Arabic.
63
+
64
+ A page that sets no `lang` uses `default_lang`. Keep every visible string in `locale.ui` so nothing in the layout stays in one language.