From d0d7d4ed83bfc9258e8f624d97e11f3dde293dbd Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 15:11:36 +0900 Subject: [PATCH 01/16] Add fallback content generation for unsupported languages --- _data/locales/en.yml | 1 + _data/locales/ko.yml | 1 + _includes/home/news_security.html | 6 +- _includes/recent_news.html | 4 +- _includes/title.html | 2 +- _layouts/news.html | 4 +- _plugins/fallback_generator.rb | 122 ++++++++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 _plugins/fallback_generator.rb diff --git a/_data/locales/en.yml b/_data/locales/en.yml index 36cd816829..80730109ff 100644 --- a/_data/locales/en.yml +++ b/_data/locales/en.yml @@ -182,6 +182,7 @@ month_names: posted_by: Posted by AUTHOR on %-d %b %Y translated_by: Translated by +fallback_notice: "This content is not currently available in the language you selected" feed: title: Ruby News diff --git a/_data/locales/ko.yml b/_data/locales/ko.yml index 63e2e14b56..16b77dd723 100644 --- a/_data/locales/ko.yml +++ b/_data/locales/ko.yml @@ -161,6 +161,7 @@ month_names: posted_by: '작성자: AUTHOR (%Y-%m-%d)' translated_by: '번역자:' +fallback_notice: "이 콘텐츠는 아직 한국어 번역이 제공되지 않아 영어로 표시됩니다" feed: title: Ruby 뉴스 diff --git a/_includes/home/news_security.html b/_includes/home/news_security.html index f44ebb86a9..face95c84c 100644 --- a/_includes/home/news_security.html +++ b/_includes/home/news_security.html @@ -46,13 +46,13 @@

{% for post in news_posts %}
-

+

{{ post.title }}

-
+
{{ post.excerpt | strip_html | truncatewords: 30 }}
@@ -86,7 +86,7 @@

{% for post in security_posts %}
-

+

{{ post.title }} diff --git a/_includes/recent_news.html b/_includes/recent_news.html index dcf8bedf4f..4c2eb26cd2 100644 --- a/_includes/recent_news.html +++ b/_includes/recent_news.html @@ -36,13 +36,13 @@

{% for post in recent_posts %}
-

+

{{ post.title }}

-

+

{{ post.excerpt | strip_html | truncatewords: 25 }}

diff --git a/_includes/title.html b/_includes/title.html index 038e38503a..68a597ec39 100644 --- a/_includes/title.html +++ b/_includes/title.html @@ -1,5 +1,5 @@ {% if page.header != null %} {{ page.header | markdownify }} {% else %} -

{{ page.title }}

+{{ page.title }}

{% endif %} diff --git a/_layouts/news.html b/_layouts/news.html index c00e1c68cc..3d63a752fc 100644 --- a/_layouts/news.html +++ b/_layouts/news.html @@ -45,13 +45,13 @@
{% for post in page.posts %}
-

+

{{ post.title }}

-
+
{{ post.excerpt | markdownify }}
diff --git a/_plugins/fallback_generator.rb b/_plugins/fallback_generator.rb new file mode 100644 index 0000000000..26de91207c --- /dev/null +++ b/_plugins/fallback_generator.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'set' + +module Jekyll + class FallbackGenerator < Generator + priority :high + + def generate(site) + @site = site + @languages = site.data['languages'].map { |l| l['code'] } + + fallback_posts + fallback_pages + end + + def fallback_posts + en_posts = @site.posts.docs.select { |p| p.data['lang'] == 'en' } + + existing_posts_by_lang = {} + @site.posts.docs.each do |post| + lang = post.data['lang'] + next unless lang + existing_posts_by_lang[lang] ||= Set.new + existing_posts_by_lang[lang] << File.basename(post.path) + end + + new_posts = [] + en_posts.each do |en_post| + filename = File.basename(en_post.path) + + @languages.each do |lang| + next if lang == 'en' + next if existing_posts_by_lang[lang]&.include?(filename) + + new_posts << create_fallback_doc(en_post, lang) + end + end + + @site.posts.docs.concat(new_posts) + @site.posts.docs.sort! + @site.instance_variable_set(:@categories, nil) + @site.instance_variable_set(:@tags, nil) + end + + def fallback_pages + en_pages = @site.pages.select { |p| p.data['lang'] == 'en' } + + existing_pages_by_lang = {} + @site.pages.each do |page| + lang = page.data['lang'] + next unless lang + existing_pages_by_lang[lang] ||= Set.new + + rel_path = page.path.sub(%r{^#{lang}/}, "") + existing_pages_by_lang[lang] << rel_path + end + + new_pages = [] + en_pages.each do |en_page| + rel_path = en_page.path.sub(%r{^en/}, "") + next if rel_path == en_page.path + + @languages.each do |lang| + next if lang == 'en' + next if existing_pages_by_lang[lang]&.include?(rel_path) + next if rel_path.end_with?(".xml") || rel_path.end_with?(".rss") + + new_pages << create_fallback_page(en_page, lang, rel_path) + end + end + @site.pages.concat(new_pages) + end + + def create_fallback_doc(en_doc, lang) + new_doc = en_doc.clone + new_doc.instance_variable_set(:@data, en_doc.data.dup) + + new_doc.data['lang'] = lang + new_doc.data['fallback'] = true + new_doc.data['content_lang'] = 'en' + new_doc.data['categories'] = [lang] + (en_doc.data['categories'] || []) - ['en'] + + new_path = en_doc.path.sub('/en/', "/#{lang}/") + new_doc.instance_variable_set(:@path, new_path) + + wrap_content(new_doc, en_doc, lang) + new_doc + end + + def create_fallback_page(en_page, lang, rel_path) + new_page = en_page.clone + new_page.instance_variable_set(:@data, en_page.data.dup) + + new_dir = File.join(lang, File.dirname(rel_path)) + new_page.instance_variable_set(:@dir, new_dir) + new_page.instance_variable_set(:@path, File.join(lang, rel_path)) + + new_page.data['lang'] = lang + new_page.data['fallback'] = true + new_page.data['content_lang'] = 'en' + + wrap_content(new_page, en_page, lang) + new_page + end + + def wrap_content(new_obj, en_obj, lang) + notice = @site.data['locales'][lang]['fallback_notice'] rescue nil + notice ||= @site.data['locales']['en']['fallback_notice'] rescue "Translated version not available" + + # Using a combination of Tailwind classes and inline styles to ensure visibility + new_obj.content = <<~HTML +
+ #{notice} +
+
+ #{en_obj.content} +
+ HTML + end + end +end From ccc1687405f38b05709015e930f811808299c99f Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 15:37:12 +0900 Subject: [PATCH 02/16] Add Tailwind classes for "sky" color palette to styles, config, and fallback notice content --- _plugins/fallback_generator.rb | 2 +- stylesheets/compiled.css | 29 +++++++++++++++++++++++++++++ tailwind.config.js | 6 ++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/_plugins/fallback_generator.rb b/_plugins/fallback_generator.rb index 26de91207c..3a1ff05f9e 100644 --- a/_plugins/fallback_generator.rb +++ b/_plugins/fallback_generator.rb @@ -110,7 +110,7 @@ def wrap_content(new_obj, en_obj, lang) # Using a combination of Tailwind classes and inline styles to ensure visibility new_obj.content = <<~HTML -
+
#{notice}
diff --git a/stylesheets/compiled.css b/stylesheets/compiled.css index 0539c6d70b..8f5ebbacdb 100644 --- a/stylesheets/compiled.css +++ b/stylesheets/compiled.css @@ -4191,6 +4191,11 @@ html:lang(ja), border-color: var(--color-gold-600); } +.border-sky-200{ + --tw-border-opacity: 1; + border-color: rgb(186 230 253 / var(--tw-border-opacity, 1)); +} + .border-stone-200{ --tw-border-opacity: 1; border-color: rgb(231 229 228 / var(--tw-border-opacity, 1)); @@ -4213,6 +4218,11 @@ html:lang(ja), background-color: var(--color-ruby-100); } +.bg-sky-50{ + --tw-bg-opacity: 1; + background-color: rgb(240 249 255 / var(--tw-bg-opacity, 1)); +} + .bg-stone-100{ --tw-bg-opacity: 1; background-color: rgb(245 245 244 / var(--tw-bg-opacity, 1)); @@ -4597,6 +4607,11 @@ html:lang(ja), color: var(--color-text-link); } +.text-sky-800{ + --tw-text-opacity: 1; + color: rgb(7 89 133 / var(--tw-text-opacity, 1)); +} + .text-stone-400{ --tw-text-opacity: 1; color: rgb(168 162 158 / var(--tw-text-opacity, 1)); @@ -5026,6 +5041,11 @@ html:lang(ja), border-color: var(--color-gold-500); } +.dark\:border-sky-800:is(.dark *){ + --tw-border-opacity: 1; + border-color: rgb(7 89 133 / var(--tw-border-opacity, 1)); +} + .dark\:border-stone-600:is(.dark *){ --tw-border-opacity: 1; border-color: rgb(87 83 78 / var(--tw-border-opacity, 1)); @@ -5044,6 +5064,10 @@ html:lang(ja), background-color: var(--color-ruby-800); } +.dark\:bg-sky-900\/30:is(.dark *){ + background-color: rgb(12 74 110 / 0.3); +} + .dark\:bg-stone-700:is(.dark *){ --tw-bg-opacity: 1; background-color: rgb(68 64 60 / var(--tw-bg-opacity, 1)); @@ -5085,6 +5109,11 @@ html:lang(ja), color: var(--color-ruby-500); } +.dark\:text-sky-200:is(.dark *){ + --tw-text-opacity: 1; + color: rgb(186 230 253 / var(--tw-text-opacity, 1)); +} + .dark\:text-stone-100:is(.dark *){ --tw-text-opacity: 1; color: rgb(245 245 244 / var(--tw-text-opacity, 1)); diff --git a/tailwind.config.js b/tailwind.config.js index 72cfbbbf04..0a6cdfafea 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -25,6 +25,12 @@ module.exports = { 'mt-2', 'text-stone-700', 'dark:text-stone-300', + 'bg-sky-50', + 'dark:bg-sky-900/30', + 'border-sky-200', + 'dark:border-sky-800', + 'text-sky-800', + 'dark:text-sky-200', 'transition-colors', // SVG fill for custom stone-770 color 'fill-stone-770', From 3c014cff2d81849c30739ac3840d32e8f645ac4a Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 15:43:17 +0900 Subject: [PATCH 03/16] Add `lang` attribute handling to TOC list items for improved language compatibility --- javascripts/toc.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/javascripts/toc.js b/javascripts/toc.js index 56e13d9adc..83c4d2c848 100644 --- a/javascripts/toc.js +++ b/javascripts/toc.js @@ -31,6 +31,7 @@ } function buildTOCHTML(headings) { + const pageLang = document.documentElement.lang; let html = '
    '; let currentLevel = 2; @@ -39,13 +40,17 @@ const text = heading.textContent; const id = heading.id; + // Check for lang attribute on heading or its ancestors + const lang = heading.getAttribute('lang') || heading.closest('[lang]')?.getAttribute('lang'); + const langAttr = (lang && lang !== pageLang) ? ` lang="${lang}"` : ''; + if (level > currentLevel) { html += '
      '; } else if (level < currentLevel) { html += '
    '; } - html += `
  • ${text}
  • `; + html += `
  • ${text}
  • `; currentLevel = level; }); From 3c6ca2eb1c7c407c4104a0962a260991b6c4815a Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 16:19:38 +0900 Subject: [PATCH 04/16] Add tests for fallback content generation and validation --- test/test_fallback_generator.rb | 205 ++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 test/test_fallback_generator.rb diff --git a/test/test_fallback_generator.rb b/test/test_fallback_generator.rb new file mode 100644 index 0000000000..4f1c7de9d2 --- /dev/null +++ b/test/test_fallback_generator.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require "helper" +require "jekyll" +require "yaml" +require_relative "../_plugins/fallback_generator" + +describe Jekyll::FallbackGenerator do + let(:actual_languages_path) { File.expand_path("../_data/languages.yml", __dir__) } + + before do + chdir_tempdir + + # 1. Setup config + create_file("source/_config.yml", <<~CONFIG) + markdown: kramdown + permalink: pretty + CONFIG + + # 2. Setup languages data from actual file + add a test language 'zz' + # 'zz' is a virtual language guaranteed to have no translation or locale file, + # ensuring the test for fallback to English notice remains robust. + actual_langs = YAML.load_file(actual_languages_path) + actual_langs << { 'code' => 'zz', 'name' => 'Test Language', 'native_name' => 'Test' } + create_file("source/_data/languages.yml", YAML.dump(actual_langs)) + + # 3. Setup locales from actual files + locales_dir = File.expand_path("../_data/locales", __dir__) + Dir.glob("#{locales_dir}/*.yml").each do |actual_locale_path| + filename = File.basename(actual_locale_path) + create_file("source/_data/locales/#{filename}", File.read(actual_locale_path)) + end + + # 4. Setup layouts + create_file("source/_layouts/default.html", "{{ content }}") + create_file("source/_layouts/news.html", "{{ content }}") + + # 5. Create an English post + create_file("source/en/news/_posts/2025-12-25-ruby-4-0-0.md", <<~MARKDOWN) + --- + layout: news + title: "Ruby 4.0.0 Released" + lang: en + author: "matz" + --- + Ruby 4.0.0 is finally here! + MARKDOWN + + # 6. Create an English page + create_file("source/en/about.md", <<~MARKDOWN) + --- + layout: default + title: "About Ruby" + lang: en + --- + Ruby is a dynamic, open source programming language. + MARKDOWN + + # 7. Create existing posts/pages to test exclusion + # Korean post + create_file("source/ko/news/_posts/2025-12-25-ruby-4-0-0.md", <<~MARKDOWN) + --- + layout: news + title: "루비 4.0.0 출시" + lang: ko + author: "matz" + --- + 기존 한국어 콘텐츠입니다. + MARKDOWN + + # Japanese content (Plausibility: Ruby is a Japanese language) + create_file("source/ja/news/_posts/2025-12-25-ruby-4-0-0.md", <<~MARKDOWN) + --- + layout: news + title: "Ruby 4.0.0 リリース" + lang: ja + author: "matz" + --- + 既存の日本語コンテンツです。 + MARKDOWN + + create_file("source/ja/about.md", <<~MARKDOWN) + --- + layout: default + title: "Rubyについて" + lang: ja + --- + Rubyは、オープンソースの動的なプログラミング言語です。 + MARKDOWN + + @config = Jekyll.configuration( + "source" => "source", + "destination" => "_site", + "quiet" => true + ) + @site = Jekyll::Site.new(@config) + @site.process + end + + after do + teardown_tempdir + end + + it "should generate fallback posts for all languages defined in languages.yml" do + @site.data['languages'].map { |l| l['code'] }.each do |lang| + next if lang == 'en' + next if lang == 'ko' # Existing one + next if lang == 'ja' # Existing one + + # Verify that the fallback post document exists in Jekyll's internal state + post = @site.posts.docs.find { |d| d.data['lang'] == lang && d.data['fallback'] } + _(post).wont_be_nil "Fallback post for #{lang} should be generated" + + # Verify the path and URL + _(post.url).must_match %r{/#{lang}/news/2025/12/25/ruby-4-0-0/} + + # Verify output file exists + file_must_exist("_site/#{lang}/news/2025/12/25/ruby-4-0-0/index.html") + end + end + + it "should NOT overwrite existing translated posts" do + # Korean + ko_post = @site.posts.docs.find { |d| d.data['lang'] == 'ko' && File.basename(d.path) == '2025-12-25-ruby-4-0-0.md' } + _(ko_post).wont_be_nil + _(ko_post.data['fallback']).must_be_nil + _(File.read("_site/ko/news/2025/12/25/ruby-4-0-0/index.html")).must_match "기존 한국어 콘텐츠입니다" + + # Japanese + ja_post = @site.posts.docs.find { |d| d.data['lang'] == 'ja' && File.basename(d.path) == '2025-12-25-ruby-4-0-0.md' } + _(ja_post).wont_be_nil + _(ja_post.data['fallback']).must_be_nil + _(File.read("_site/ja/news/2025/12/25/ruby-4-0-0/index.html")).must_match "既存の日本語コンテンツです" + end + + it "should generate fallback pages for all languages defined in languages.yml" do + @site.data['languages'].map { |l| l['code'] }.each do |lang| + next if lang == 'en' + next if lang == 'ja' # Existing one + + page = @site.pages.find { |p| p.data['lang'] == lang && p.data['fallback'] && p.path.include?("about.md") } + _(page).wont_be_nil "Fallback page for #{lang} should be generated" + file_must_exist("_site/#{lang}/about/index.html") + end + end + + it "should wrap content with notice box and lang='en' tag" do + # Check Korean fallback page (to test localization) + # ko/about.md doesn't exist in source, so it should be generated as a fallback + ko_page = File.read("_site/ko/about/index.html") + _(ko_page).must_match "fallback-notice" + _(ko_page).must_match "bg-sky-50" + # Actual content from ko.yml + _(ko_page).must_match "이 콘텐츠는 아직 한국어 번역이 제공되지 않아 영어로 표시됩니다" + _(ko_page).must_match "
    Date: Thu, 25 Dec 2025 16:20:03 +0900 Subject: [PATCH 05/16] Add tests for fallback generator plugin and extend default test task --- Rakefile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 74a84954d6..8a4a8e7047 100644 --- a/Rakefile +++ b/Rakefile @@ -14,7 +14,7 @@ CONFIG = "_config.yml" task default: [:build] desc "Run tests (test-linter, lint, build)" -task test: %i[test-news-plugin test-linter lint build] +task test: %i[test-news-plugin test-fallback-generator test-linter lint build] desc "Build the Jekyll site" task :build do @@ -129,3 +129,11 @@ Rake::TestTask.new(:"test-news-plugin") do |t| t.test_files = FileList['test/test_plugin_news.rb'] t.verbose = true end + +require "rake/testtask" +Rake::TestTask.new(:"test-fallback-generator") do |t| + t.description = "Run tests for the fallback generator plugin" + t.libs = ["test"] + t.test_files = FileList['test/test_fallback_generator.rb'] + t.verbose = true +end From 9eca3eaa159087e741374d43c9240b90c3a059ee Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 16:29:22 +0900 Subject: [PATCH 06/16] Update test fallback generator to replace 'zz' with 'xz' as test language --- test/test_fallback_generator.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/test_fallback_generator.rb b/test/test_fallback_generator.rb index 4f1c7de9d2..fb59187b27 100644 --- a/test/test_fallback_generator.rb +++ b/test/test_fallback_generator.rb @@ -17,11 +17,11 @@ permalink: pretty CONFIG - # 2. Setup languages data from actual file + add a test language 'zz' - # 'zz' is a virtual language guaranteed to have no translation or locale file, + # 2. Setup languages data from actual file + add a test language 'xz' + # 'xz' is a virtual language guaranteed to have no translation or locale file, # ensuring the test for fallback to English notice remains robust. actual_langs = YAML.load_file(actual_languages_path) - actual_langs << { 'code' => 'zz', 'name' => 'Test Language', 'native_name' => 'Test' } + actual_langs << { 'code' => 'xz', 'name' => 'Test Language', 'native_name' => 'Test' } create_file("source/_data/languages.yml", YAML.dump(actual_langs)) # 3. Setup locales from actual files @@ -157,9 +157,9 @@ end it "should use English notice when specific locale notice is missing" do - # 'zz' is a virtual language with no locale file, so it should fallback to English notice - zz_page = File.read("_site/zz/about/index.html") - _(zz_page).must_match "This content is not currently available in the language you selected" + # 'xz' is a virtual language with no locale file, so it should fallback to English notice + xz_page = File.read("_site/xz/about/index.html") + _(xz_page).must_match "This content is not currently available in the language you selected" end it "should not have a period at the end of the notice" do @@ -177,9 +177,9 @@ end it "should correctly set categories for fallback posts" do - zz_post = @site.posts.docs.find { |d| d.data['lang'] == 'zz' && d.data['fallback'] } - _(zz_post.data['categories']).must_include 'zz' - _(zz_post.data['categories']).wont_include 'en' + xz_post = @site.posts.docs.find { |d| d.data['lang'] == 'xz' && d.data['fallback'] } + _(xz_post.data['categories']).must_include 'xz' + _(xz_post.data['categories']).wont_include 'en' end it "should maintain post sorting after adding fallback posts" do From be36f3317eea6998172738c9a9cedd6072caff09 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 16:57:25 +0900 Subject: [PATCH 07/16] Standardize spacing around `lang` attribute in fallback conditionals --- _includes/home/news_security.html | 4 ++-- _includes/recent_news.html | 2 +- _layouts/news.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/_includes/home/news_security.html b/_includes/home/news_security.html index face95c84c..503d838fa9 100644 --- a/_includes/home/news_security.html +++ b/_includes/home/news_security.html @@ -52,7 +52,7 @@

    -
    +
    {{ post.excerpt | strip_html | truncatewords: 30 }}
    @@ -86,7 +86,7 @@

    {% for post in security_posts %}
    -

    +

    {{ post.title }} diff --git a/_includes/recent_news.html b/_includes/recent_news.html index 4c2eb26cd2..a27fbd6225 100644 --- a/_includes/recent_news.html +++ b/_includes/recent_news.html @@ -42,7 +42,7 @@

    -

    +

    {{ post.excerpt | strip_html | truncatewords: 25 }}

    diff --git a/_layouts/news.html b/_layouts/news.html index 3d63a752fc..e5e1f4121e 100644 --- a/_layouts/news.html +++ b/_layouts/news.html @@ -51,7 +51,7 @@

    -
    +
    {{ post.excerpt | markdownify }}
    From 52cb3b1fcc818f6d89a13c4b7e915f4f6138949e Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 17:51:56 +0900 Subject: [PATCH 08/16] Refactor `lang` attribute selectors to exclude nested conflicting language contexts. --- stylesheets/compiled.css | 34 +++++++++++++++++----------------- stylesheets/tailwind.css | 32 ++++++++++++++++---------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/stylesheets/compiled.css b/stylesheets/compiled.css index 8f5ebbacdb..8cf0065537 100644 --- a/stylesheets/compiled.css +++ b/stylesheets/compiled.css @@ -1962,7 +1962,7 @@ video { display: none; } -body:lang(ja),body:lang(ko),body:lang(zh-CN),body:lang(zh-TW) { +body:lang(ja):not([lang] *:not([lang="ja"])),body:lang(ko):not([lang] *:not([lang="ko"])),body:lang(zh-CN):not([lang] *:not([lang="zh-CN"])),body:lang(zh-TW):not([lang] *:not([lang="zh-TW"])) { font-family: "Plus Jakarta Sans", var(--noto-sans-subset), -apple-system, BlinkMacSystemFont, sans-serif; } @@ -1986,25 +1986,25 @@ body:is(.dark *){ /* CJK fonts */ -html:lang(ja), - body:lang(ja), - .font-default:lang(ja), - .font-sans:lang(ja), +html:lang(ja):not([lang] *:not([lang="ja"])), + body:lang(ja):not([lang] *:not([lang="ja"])), + .font-default:lang(ja):not([lang] *:not([lang="ja"])), + .font-sans:lang(ja):not([lang] *:not([lang="ja"])), [lang="ja"], - html:lang(ko), - body:lang(ko), - .font-default:lang(ko), - .font-sans:lang(ko), + html:lang(ko):not([lang] *:not([lang="ko"])), + body:lang(ko):not([lang] *:not([lang="ko"])), + .font-default:lang(ko):not([lang] *:not([lang="ko"])), + .font-sans:lang(ko):not([lang] *:not([lang="ko"])), [lang="ko"], - html:lang(zh-CN), - body:lang(zh-CN), - .font-default:lang(zh-CN), - .font-sans:lang(zh-CN), + html:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), + body:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), + .font-default:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), + .font-sans:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), [lang="zh-CN"], - html:lang(zh-TW), - body:lang(zh-TW), - .font-default:lang(zh-TW), - .font-sans:lang(zh-TW), + html:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), + body:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), + .font-default:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), + .font-sans:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), [lang="zh-TW"] { font-family: "Plus Jakarta Sans", var(--noto-sans-subset), -apple-system, BlinkMacSystemFont, sans-serif; } diff --git a/stylesheets/tailwind.css b/stylesheets/tailwind.css index 40bd2d9522..c4954e922e 100644 --- a/stylesheets/tailwind.css +++ b/stylesheets/tailwind.css @@ -25,25 +25,25 @@ } /* CJK fonts */ - html:lang(ja), - body:lang(ja), - .font-default:lang(ja), - .font-sans:lang(ja), + html:lang(ja):not([lang] *:not([lang="ja"])), + body:lang(ja):not([lang] *:not([lang="ja"])), + .font-default:lang(ja):not([lang] *:not([lang="ja"])), + .font-sans:lang(ja):not([lang] *:not([lang="ja"])), [lang="ja"], - html:lang(ko), - body:lang(ko), - .font-default:lang(ko), - .font-sans:lang(ko), + html:lang(ko):not([lang] *:not([lang="ko"])), + body:lang(ko):not([lang] *:not([lang="ko"])), + .font-default:lang(ko):not([lang] *:not([lang="ko"])), + .font-sans:lang(ko):not([lang] *:not([lang="ko"])), [lang="ko"], - html:lang(zh-CN), - body:lang(zh-CN), - .font-default:lang(zh-CN), - .font-sans:lang(zh-CN), + html:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), + body:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), + .font-default:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), + .font-sans:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), [lang="zh-CN"], - html:lang(zh-TW), - body:lang(zh-TW), - .font-default:lang(zh-TW), - .font-sans:lang(zh-TW), + html:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), + body:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), + .font-default:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), + .font-sans:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), [lang="zh-TW"] { font-family: "Plus Jakarta Sans", var(--noto-sans-subset), -apple-system, BlinkMacSystemFont, sans-serif; } From bd3c1b62b47c143698551e4510072bfa9ad6046a Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 19:13:07 +0900 Subject: [PATCH 09/16] Simplify `lang` attribute selectors and consolidate logic for improved readability and maintainability. --- stylesheets/compiled.css | 38 +++++++++++++++++++++----------------- stylesheets/tailwind.css | 32 ++++++++++++++++---------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/stylesheets/compiled.css b/stylesheets/compiled.css index 8cf0065537..aebb5dae8d 100644 --- a/stylesheets/compiled.css +++ b/stylesheets/compiled.css @@ -1962,7 +1962,7 @@ video { display: none; } -body:lang(ja):not([lang] *:not([lang="ja"])),body:lang(ko):not([lang] *:not([lang="ko"])),body:lang(zh-CN):not([lang] *:not([lang="zh-CN"])),body:lang(zh-TW):not([lang] *:not([lang="zh-TW"])) { +body:lang(ja),body:lang(ko),body:lang(zh-CN),body:lang(zh-TW) { font-family: "Plus Jakarta Sans", var(--noto-sans-subset), -apple-system, BlinkMacSystemFont, sans-serif; } @@ -1984,27 +1984,31 @@ body:is(.dark *){ color: rgb(250 250 249 / var(--tw-text-opacity, 1)); } +[lang]:lang(ja),[lang]:lang(ko),[lang]:lang(zh-CN),[lang]:lang(zh-TW) { + font-family: "Plus Jakarta Sans", var(--noto-sans-subset), -apple-system, BlinkMacSystemFont, sans-serif; +} + +[lang]{ + font-family: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, sans-serif; +} + /* CJK fonts */ -html:lang(ja):not([lang] *:not([lang="ja"])), - body:lang(ja):not([lang] *:not([lang="ja"])), - .font-default:lang(ja):not([lang] *:not([lang="ja"])), - .font-sans:lang(ja):not([lang] *:not([lang="ja"])), +html:lang(ja), + .font-default:lang(ja), + .font-sans:lang(ja), [lang="ja"], - html:lang(ko):not([lang] *:not([lang="ko"])), - body:lang(ko):not([lang] *:not([lang="ko"])), - .font-default:lang(ko):not([lang] *:not([lang="ko"])), - .font-sans:lang(ko):not([lang] *:not([lang="ko"])), + html:lang(ko), + .font-default:lang(ko), + .font-sans:lang(ko), [lang="ko"], - html:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), - body:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), - .font-default:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), - .font-sans:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), + html:lang(zh-CN), + .font-default:lang(zh-CN), + .font-sans:lang(zh-CN), [lang="zh-CN"], - html:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), - body:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), - .font-default:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), - .font-sans:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), + html:lang(zh-TW), + .font-default:lang(zh-TW), + .font-sans:lang(zh-TW), [lang="zh-TW"] { font-family: "Plus Jakarta Sans", var(--noto-sans-subset), -apple-system, BlinkMacSystemFont, sans-serif; } diff --git a/stylesheets/tailwind.css b/stylesheets/tailwind.css index c4954e922e..8c60b5843c 100644 --- a/stylesheets/tailwind.css +++ b/stylesheets/tailwind.css @@ -24,26 +24,26 @@ @apply dark:bg-stone-900 dark:text-stone-50; } + [lang] { + @apply font-default; + } + /* CJK fonts */ - html:lang(ja):not([lang] *:not([lang="ja"])), - body:lang(ja):not([lang] *:not([lang="ja"])), - .font-default:lang(ja):not([lang] *:not([lang="ja"])), - .font-sans:lang(ja):not([lang] *:not([lang="ja"])), + html:lang(ja), + .font-default:lang(ja), + .font-sans:lang(ja), [lang="ja"], - html:lang(ko):not([lang] *:not([lang="ko"])), - body:lang(ko):not([lang] *:not([lang="ko"])), - .font-default:lang(ko):not([lang] *:not([lang="ko"])), - .font-sans:lang(ko):not([lang] *:not([lang="ko"])), + html:lang(ko), + .font-default:lang(ko), + .font-sans:lang(ko), [lang="ko"], - html:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), - body:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), - .font-default:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), - .font-sans:lang(zh-CN):not([lang] *:not([lang="zh-CN"])), + html:lang(zh-CN), + .font-default:lang(zh-CN), + .font-sans:lang(zh-CN), [lang="zh-CN"], - html:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), - body:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), - .font-default:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), - .font-sans:lang(zh-TW):not([lang] *:not([lang="zh-TW"])), + html:lang(zh-TW), + .font-default:lang(zh-TW), + .font-sans:lang(zh-TW), [lang="zh-TW"] { font-family: "Plus Jakarta Sans", var(--noto-sans-subset), -apple-system, BlinkMacSystemFont, sans-serif; } From 143746a29d6022c31bcc500d8c71077c06f7d8b0 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 19:27:51 +0900 Subject: [PATCH 10/16] Add `body` selectors for CJK `lang` attribute to ensure consistent font application --- stylesheets/tailwind.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stylesheets/tailwind.css b/stylesheets/tailwind.css index 8c60b5843c..d2a19a7b87 100644 --- a/stylesheets/tailwind.css +++ b/stylesheets/tailwind.css @@ -30,18 +30,22 @@ /* CJK fonts */ html:lang(ja), + body:lang(ja), .font-default:lang(ja), .font-sans:lang(ja), [lang="ja"], html:lang(ko), + body:lang(ko), .font-default:lang(ko), .font-sans:lang(ko), [lang="ko"], html:lang(zh-CN), + body:lang(zh-CN), .font-default:lang(zh-CN), .font-sans:lang(zh-CN), [lang="zh-CN"], html:lang(zh-TW), + body:lang(zh-TW), .font-default:lang(zh-TW), .font-sans:lang(zh-TW), [lang="zh-TW"] { From 913fcf28e9ef9a2ce53e52094409e90020ebc700 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 22:17:53 +0900 Subject: [PATCH 11/16] Add `body` selectors to CJK `lang` rules for complete font coverage --- stylesheets/compiled.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stylesheets/compiled.css b/stylesheets/compiled.css index aebb5dae8d..e338fa2301 100644 --- a/stylesheets/compiled.css +++ b/stylesheets/compiled.css @@ -1995,18 +1995,22 @@ body:is(.dark *){ /* CJK fonts */ html:lang(ja), + body:lang(ja), .font-default:lang(ja), .font-sans:lang(ja), [lang="ja"], html:lang(ko), + body:lang(ko), .font-default:lang(ko), .font-sans:lang(ko), [lang="ko"], html:lang(zh-CN), + body:lang(zh-CN), .font-default:lang(zh-CN), .font-sans:lang(zh-CN), [lang="zh-CN"], html:lang(zh-TW), + body:lang(zh-TW), .font-default:lang(zh-TW), .font-sans:lang(zh-TW), [lang="zh-TW"] { From 382889b767b0a1857b813e0e6a15672a755275d3 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 23:12:10 +0900 Subject: [PATCH 12/16] Exclude posts with `fallback` attribute from translation status tracking --- _plugins/translation_status.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/_plugins/translation_status.rb b/_plugins/translation_status.rb index 43a456ce01..751c6bb42a 100644 --- a/_plugins/translation_status.rb +++ b/_plugins/translation_status.rb @@ -113,6 +113,7 @@ def render(context) LANGS.each do |lang| categories[lang].each do |post| next if too_old(post.date) + next if post.data["fallback"] name = post.url.gsub(%r{\A/#{lang}/news/}, "") @posts[name].translations << lang From 04c2569bb1a2aaf8cab8eae4f9a157e86015777e Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 23:42:32 +0900 Subject: [PATCH 13/16] Update `translation_status.rb` to fix fallback handling and add `uk` to LANGS --- _plugins/translation_status.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/_plugins/translation_status.rb b/_plugins/translation_status.rb index 751c6bb42a..120ed34746 100644 --- a/_plugins/translation_status.rb +++ b/_plugins/translation_status.rb @@ -8,7 +8,7 @@ module Jekyll # Outputs HTML. module TranslationStatus - LANGS = %w[en bg de es fr id it ja ko pl pt ru tr ua vi zh_cn zh_tw].freeze + LANGS = %w[en bg de es fr id it ja ko pl pt ru tr uk vi zh_cn zh_tw].freeze START_DATE = "2013-04-01" OK_CHAR = "✓" @@ -107,17 +107,21 @@ def table_row(post) end def render(context) + @posts = Hash.new {|posts, name| posts[name] = Post.new(name) } categories = context.registers[:site].categories ignored_langs = categories.keys - LANGS - ["news"] LANGS.each do |lang| - categories[lang].each do |post| + (categories[lang] || []).each do |post| next if too_old(post.date) - next if post.data["fallback"] + if post.data["fallback"] + # puts "DEBUG: Skipping fallback post #{post.url} for lang #{lang}" + next + end name = post.url.gsub(%r{\A/#{lang}/news/}, "") @posts[name].translations << lang - @posts[name].security = true if post.data["tags"].include?("security") + @posts[name].security = true if post.data["tags"] && post.data["tags"].include?("security") end end From 6b925cbdd3eaeaf3069703e5875ba43716f0ec9e Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 23:43:02 +0900 Subject: [PATCH 14/16] Add tests for `translation_status` plugin and extend default test task --- Rakefile | 10 +++- test/test_translation_status.rb | 96 +++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 test/test_translation_status.rb diff --git a/Rakefile b/Rakefile index 8a4a8e7047..5fa60aaab8 100644 --- a/Rakefile +++ b/Rakefile @@ -14,7 +14,7 @@ CONFIG = "_config.yml" task default: [:build] desc "Run tests (test-linter, lint, build)" -task test: %i[test-news-plugin test-fallback-generator test-linter lint build] +task test: %i[test-news-plugin test-fallback-generator test-translation-status test-linter lint build] desc "Build the Jekyll site" task :build do @@ -137,3 +137,11 @@ Rake::TestTask.new(:"test-fallback-generator") do |t| t.test_files = FileList['test/test_fallback_generator.rb'] t.verbose = true end + +require "rake/testtask" +Rake::TestTask.new(:"test-translation-status") do |t| + t.description = "Run tests for the translation status plugin" + t.libs = ["test"] + t.test_files = FileList['test/test_translation_status.rb'] + t.verbose = true +end diff --git a/test/test_translation_status.rb b/test/test_translation_status.rb new file mode 100644 index 0000000000..74925ccead --- /dev/null +++ b/test/test_translation_status.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "helper" +require "jekyll" +require_relative "../_plugins/translation_status" +require_relative "../_plugins/fallback_generator" + +describe Jekyll::TranslationStatus do + let(:actual_languages_path) { File.expand_path("../_data/languages.yml", __dir__) } + + before do + chdir_tempdir + + # 1. Setup languages data from actual file + create_file("source/_data/languages.yml", File.read(actual_languages_path)) + + # 2. Setup locales from actual files + locales_dir = File.expand_path("../_data/locales", __dir__) + Dir.glob("#{locales_dir}/*.yml").each do |actual_locale_path| + filename = File.basename(actual_locale_path) + create_file("source/_data/locales/#{filename}", File.read(actual_locale_path)) + end + + # Setup layouts + create_file("source/_layouts/news.html", "{{ content }}") + create_file("source/_layouts/news_post.html", "{{ content }}") + + # Create English post (after START_DATE) + create_file("source/en/news/_posts/2025-01-01-test-post.md", <<~MARKDOWN) + --- + layout: news_post + title: "English Post" + lang: en + --- + Content + MARKDOWN + + # Create Japanese post (real translation) + create_file("source/ja/news/_posts/2025-01-01-test-post.md", <<~MARKDOWN) + --- + layout: news_post + title: "Japanese Translation" + lang: ja + --- + Content + MARKDOWN + + # French post is missing, will be generated as fallback + + @config = Jekyll.configuration( + "source" => "source", + "destination" => "_site", + "quiet" => true + ) + @site = Jekyll::Site.new(@config) + @site.process + end + + after do + teardown_tempdir + end + + it "should NOT count fallback posts as translated" do + # Verify fallback post for French was created + fr_post = @site.posts.docs.find { |d| d.data['lang'] == 'fr' && d.data['fallback'] } + _(fr_post).wont_be_nil + + # Render twice to check for data leakage + template = Liquid::Template.parse("{% translation_status %}") + template.render!({}, registers: { site: @site }) + result = template.render!({}, registers: { site: @site }) + + # 'fr' should NOT have the OK_CHAR (✓) for this post + + # Let's see the LANGS in the plugin + langs = Jekyll::TranslationStatus::LANGS + _(langs).must_include "fr" + _(langs).must_include "ja" + + # Find the row for our post + rows = result.scan(/.*?<\/td><\/tr>/m) + post_row = rows.find { |r| r.include?("test-post") } + _(post_row).wont_be_nil + + # Check for ✓ in ja and fr columns + ja_idx = langs.index("ja") + fr_idx = langs.index("fr") + uk_idx = langs.index("uk") + + cells = post_row.scan(/(.*?)<\/td>/).flatten + + _(cells[ja_idx + 1]).must_include "✓" # Japanese is translated + _(cells[fr_idx + 1]).wont_include "✓" # French is a fallback, should be empty + _(uk_idx).wont_be_nil # Ukrainian should be present as 'uk' + end +end From 56ea7edef552b03d281230fe7d4173bcfd7399c1 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 25 Dec 2025 23:53:09 +0900 Subject: [PATCH 15/16] Update Japanese test post content and title in `translation_status` tests --- test/test_translation_status.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_translation_status.rb b/test/test_translation_status.rb index 74925ccead..3acee852f9 100644 --- a/test/test_translation_status.rb +++ b/test/test_translation_status.rb @@ -39,10 +39,10 @@ create_file("source/ja/news/_posts/2025-01-01-test-post.md", <<~MARKDOWN) --- layout: news_post - title: "Japanese Translation" + title: "日本語のポスト" lang: ja --- - Content + コンテント MARKDOWN # French post is missing, will be generated as fallback From 9795c1e6dbbfaffd9ff98a7588678a173a56e22f Mon Sep 17 00:00:00 2001 From: user Date: Fri, 26 Dec 2025 00:11:59 +0900 Subject: [PATCH 16/16] Extend linter exclusions to include `node_modules` and `_site` directories --- lib/linter.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/linter.rb b/lib/linter.rb index 7b7baa49e5..0d9224eb1b 100644 --- a/lib/linter.rb +++ b/lib/linter.rb @@ -18,7 +18,9 @@ class Linter %r{\Aadmin/index\.md}, %r{\A[^/]*/examples/}, %r{\A_includes/}, - %r{\Atest/} + %r{\Atest/}, + %r{\Anode_modules/}, + %r{\A_site/} ].freeze WHITESPACE_EXCLUSIONS = [