<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Multilingual on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/tags/multilingual/</link><description>Recent content in Multilingual on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 10 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/tags/multilingual/index.xml" rel="self" type="application/rss+xml"/><item><title>Building a Bilingual Hugo Blog — Automated Korean-English Publishing Pipeline</title><link>https://ice-ice-bear.github.io/posts/2026-04-10-bilingual-hugo/</link><pubDate>Fri, 10 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-04-10-bilingual-hugo/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Building a Bilingual Hugo Blog — Automated Korean-English Publishing Pipeline" /&gt;&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;A blog that exists in only one language reaches half its audience. Today I built a bilingual publishing pipeline for Hugo that routes posts to language-specific directories, generates per-language cover images with localized titles, and links translation pairs automatically — all from a single &lt;code&gt;--language&lt;/code&gt; flag on the CLI.&lt;/p&gt;
&lt;h2 id="hugos-two-translation-methods"&gt;Hugo&amp;rsquo;s Two Translation Methods
&lt;/h2&gt;&lt;p&gt;Hugo supports multilingual content through two approaches:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;By filename&lt;/strong&gt;: &lt;code&gt;about.en.md&lt;/code&gt; / &lt;code&gt;about.ko.md&lt;/code&gt; in the same directory. Simple for small sites, but filenames get cluttered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;By content directory&lt;/strong&gt;: &lt;code&gt;content/en/posts/&lt;/code&gt; / &lt;code&gt;content/ko/posts/&lt;/code&gt; with separate directory trees per language. Better for CLI automation — the language is a routing decision, not a naming convention.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 subgraph "By Filename"
 D1["content/posts/"] --&gt; F1["post.en.md"]
 D1 --&gt; F2["post.ko.md"]
 end

 subgraph "By Content Directory"
 D2["content/en/posts/"] --&gt; F3["post.md"]
 D3["content/ko/posts/"] --&gt; F4["post.md"]
 end

 style D2 fill:#e8f5e9
 style D3 fill:#e8f5e9&lt;/pre&gt;&lt;p&gt;log-blog uses the content directory approach. The config maps language codes to directories:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;default_language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;language_content_dirs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ko&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;content/ko/posts&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;content/en/posts&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="content-routing-content_path_for"&gt;Content Routing: &lt;code&gt;content_path_for()&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The routing function is minimal — a dict lookup with a fallback:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlogConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;content_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content/posts&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# fallback&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;language_content_dirs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default_language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;content_path_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_language&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;language_content_dirs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo_path_resolved&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;language_content_dirs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When &lt;code&gt;publish --language ko&lt;/code&gt; is called, the post lands in &lt;code&gt;content/ko/posts/&lt;/code&gt;. Without &lt;code&gt;--language&lt;/code&gt;, it defaults to the &lt;code&gt;default_language&lt;/code&gt; setting. If the language has no mapping in &lt;code&gt;language_content_dirs&lt;/code&gt;, it falls back to the generic &lt;code&gt;content_dir&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This design means adding a third language (e.g., Japanese) is a one-line config change — no code modifications needed.&lt;/p&gt;
&lt;h2 id="per-language-cover-images"&gt;Per-Language Cover Images
&lt;/h2&gt;&lt;p&gt;Each language gets its own cover image with the title rendered in that language:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;static/images/posts/2026-04-10-firecrawl/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── cover-en.jpg ← &amp;#34;Deep Docs Crawling with Firecrawl&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── cover-ko.jpg ← &amp;#34;Firecrawl을 활용한 딥 문서 크롤링&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The image generator in &lt;code&gt;image_handler.py&lt;/code&gt; appends the language suffix:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cover_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cover-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.jpg&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;cover.jpg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;rel_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/images/posts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;post_slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cover_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The CLI auto-injects the correct &lt;code&gt;image:&lt;/code&gt; frontmatter path — users never write it manually. When &lt;code&gt;--cover-title &amp;quot;Korean Title&amp;quot; --language ko&lt;/code&gt; is passed, the generated image shows Korean text with tag pills, and the frontmatter points to &lt;code&gt;cover-ko.jpg&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 PUB["publish command"] --&gt; |"--language ko"| ROUTE["content_path_for('ko')"]
 PUB --&gt; |"--cover-title '한국어 제목'"| IMG["generate_cover_image()"]

 ROUTE --&gt; DIR["content/ko/posts/post.md"]
 IMG --&gt; COVER["static/.../cover-ko.jpg"]

 PUB2["publish command"] --&gt; |"--language en"| ROUTE2["content_path_for('en')"]
 PUB2 --&gt; |"--cover-title 'English Title'"| IMG2["generate_cover_image()"]

 ROUTE2 --&gt; DIR2["content/en/posts/post.md"]
 IMG2 --&gt; COVER2["static/.../cover-en.jpg"]

 DIR -.-&gt; |"same filename"| HUGO["Hugo links as &amp;lt;br/&amp;gt; translation pair"]
 DIR2 -.-&gt; HUGO&lt;/pre&gt;&lt;h2 id="hugo-configuration-hascjklanguage-matters"&gt;Hugo Configuration: &lt;code&gt;hasCJKLanguage&lt;/code&gt; Matters
&lt;/h2&gt;&lt;p&gt;One critical Hugo setting for Korean content:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hasCJKLanguage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Without this, Hugo calculates &lt;code&gt;.Summary&lt;/code&gt; and &lt;code&gt;.WordCount&lt;/code&gt; using space-based word splitting — which produces nonsensical results for Korean, Chinese, and Japanese text. With it enabled, Hugo uses CJK-aware segmentation.&lt;/p&gt;
&lt;p&gt;The Stack theme provides built-in Korean language support. Menu items translate automatically when configured under &lt;code&gt;languages.ko.menu&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ko&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;languageName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;한국어&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;포스트&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/posts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;카테고리&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/categories&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;태그&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/tags&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-translation-workflow"&gt;The Translation Workflow
&lt;/h2&gt;&lt;p&gt;The publishing flow for a bilingual post:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Write the original&lt;/strong&gt; (typically English)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rewrite for Korean audience&lt;/strong&gt; — not literal translation, but restructuring for natural Korean flow. Technical terms stay in English where conventional in Korean tech writing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publish both with same filename&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# English version → content/en/posts/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv run log-blog publish post-en.md &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cover-title &lt;span class="s2"&gt;&amp;#34;English Title&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --tags &lt;span class="s2"&gt;&amp;#34;hugo,i18n&amp;#34;&lt;/span&gt; --language en
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Korean version → content/ko/posts/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv run log-blog publish post-ko.md &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cover-title &lt;span class="s2"&gt;&amp;#34;한국어 제목&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --tags &lt;span class="s2"&gt;&amp;#34;hugo,i18n&amp;#34;&lt;/span&gt; --language ko
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hugo automatically detects that both files share the same filename and displays a language switcher on the post page. The &lt;code&gt;.Translations&lt;/code&gt; template variable handles the linking.&lt;/p&gt;
&lt;h3 id="translation-guidelines"&gt;Translation Guidelines
&lt;/h3&gt;&lt;p&gt;Key rules for the Korean rewrite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Translate&lt;/strong&gt;: title, description, body text, Mermaid labels, section headers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep unchanged&lt;/strong&gt;: tags, categories, code blocks, URLs, CLI commands&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t include &lt;code&gt;image:&lt;/code&gt;&lt;/strong&gt; — the CLI auto-injects the per-language path&lt;/li&gt;
&lt;li&gt;Mermaid safety rules (entities, quoted slashes) apply identically in both languages&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="github-multi-account-ssh-setup"&gt;GitHub Multi-Account SSH Setup
&lt;/h2&gt;&lt;p&gt;One complication: the blog repo (&lt;code&gt;ice-ice-bear&lt;/code&gt;) uses a different GitHub account than the main dev account (&lt;code&gt;lazy-mango&lt;/code&gt;). SSH key-based routing solves this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# ~/.ssh/config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Host github-blog
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HostName github.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; User git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; IdentityFile ~/.ssh/id_ed25519_blog
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The blog repo&amp;rsquo;s remote URL uses the alias: &lt;code&gt;git@github-blog:ice-ice-bear/ice-ice-bear.github.io.git&lt;/code&gt;. GitHub maps SSH keys 1:1 to accounts, so the alias ensures the correct key (and account) is selected for push operations.&lt;/p&gt;
&lt;h2 id="insights"&gt;Insights
&lt;/h2&gt;&lt;p&gt;Hugo&amp;rsquo;s multilingual support is mature but documentation-heavy — the &amp;ldquo;by content directory&amp;rdquo; vs &amp;ldquo;by filename&amp;rdquo; decision has cascading effects on your entire publishing workflow. For a CLI-driven pipeline, content directories win decisively: the language becomes a routing parameter rather than a naming convention baked into every file.&lt;/p&gt;
&lt;p&gt;The per-language cover image pattern turned out to be more important than expected. Social media previews (Open Graph, Twitter Cards) show the cover image — having &amp;ldquo;Deep Docs Crawling&amp;rdquo; on the thumbnail while the post is in Korean creates a jarring disconnect. Localized cover images make shared links feel native in each language community.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;hasCJKLanguage&lt;/code&gt; flag is the kind of setting that&amp;rsquo;s invisible until it breaks. Korean &lt;code&gt;.Summary&lt;/code&gt; without it produces garbled word counts and truncated previews. It&amp;rsquo;s a one-line fix, but discovering the problem requires actually testing with CJK content — English-only development would never surface it.&lt;/p&gt;
&lt;p&gt;What surprised me most is how little code the bilingual support required. The core routing is a dict lookup. The cover image is a filename suffix. The translation linking is Hugo&amp;rsquo;s built-in behavior when files share a name. The complexity isn&amp;rsquo;t in the implementation — it&amp;rsquo;s in knowing which Hugo features to combine and which settings matter for non-Latin scripts.&lt;/p&gt;</description></item></channel></rss>