Repository: cpp-gamedev/learn-vulkan
Pull request #10: Preliminary multilingual support
Author: login: Mes0903
Created: 2025-03-27T17:55:14Z | Merged: 2025-03-28T16:53:25Z

mdBook does not natively support multilingual documentation. A common approach is to use a third-party library like mdbook-i18n-helper, which relies on a Gettext-based workflow to enable multilingual support.

However, I believe such a complex system is unnecessary for our case. Therefore, this commit follows the approach suggested in the official mdBook documentation: overriding the `index.hbs` template to add a button that redirects users to the corresponding documentation in another language, thus achieving basic multilingual support.

This commit introduces the following three files under the theme directory:

- `index.hbs`: Adds a redirect button for switching languages.
- `lang-toggle.css`: Defines the styling for the button and the dropdown menu.
- `lang-toggle.js`: Implements the main logic behind the button's behavior.

As a result, two separate books need to be built. However, since mdBook’s `book.toml` does not support multiple `[book]` sections in a single configuration, this commit introduces a configuration template and a CMake script that generates language-specific toml files for each version, which are then built into their respective output folders.

Besides, since translations in other languages are expected to be placed under `src`, all original guide content has been moved into `src/en`.

[1]: https://github.com/google/mdbook-i18n-helpers


Diff:
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index a42b725..52f50ff 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -24,7 +24,7 @@ jobs:
       - name: build book
         run: |
           cd guide
-          mdbook build
+          cmake -P build.cmake
           ls book
       - name: setup pages
         uses: actions/configure-pages@v4
diff --git a/guide/book.toml b/guide/book.toml
index 87209f2..0c64e99 100644
--- a/guide/book.toml
+++ b/guide/book.toml
@@ -1,6 +1,10 @@
 [book]
 authors = ["Karn Kaul"]
 language = "en"
-multilingual = false
 src = "src"
 title = "Learn Vulkan"
+
+[output.html]
+theme = "theme"
+additional-js = ["theme/lang_toggle.js"]
+additional-css = ["theme/lang_toggle.css"]
diff --git a/guide/build.cmake b/guide/build.cmake
new file mode 100644
index 0000000..acf4de4
--- /dev/null
+++ b/guide/build.cmake
@@ -0,0 +1,26 @@
+# Build the target languages
+function(BuildBook LANGUAGE SOURCE_DIR TARGET_DIR)
+  set(LANGUAGE "${LANGUAGE}")
+
+  if(NOT EXISTS "${SOURCE_DIR}/src/SUMMARY.md")
+    message(WARNING "Skipping '${LANGUAGE}' – SUMMARY.md not found at ${SOURCE_DIR}")
+    return()
+  endif()
+  
+  if(NOT EXISTS "${SOURCE_DIR}/book.toml")
+    message(WARNING "Skipping '${LANGUAGE}' – book.toml not found at ${SOURCE_DIR}")
+    return()
+  endif()
+
+  message(STATUS "Building book for language: ${LANGUAGE}")
+  execute_process(
+    COMMAND mdbook build -d ${TARGET_DIR}
+    WORKING_DIRECTORY ${SOURCE_DIR}
+  )
+endfunction()
+
+# Copy the theme folder 
+file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/theme" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/translations")
+
+BuildBook("en" "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/book")
+BuildBook("zh-TW" "${CMAKE_CURRENT_SOURCE_DIR}/translations/zh-TW" "${CMAKE_CURRENT_SOURCE_DIR}/book/zh-TW")
diff --git a/guide/theme/index.hbs b/guide/theme/index.hbs
new file mode 100644
index 0000000..3827745
--- /dev/null
+++ b/guide/theme/index.hbs
@@ -0,0 +1,332 @@
+<!DOCTYPE HTML>
+<html lang="{{ language }}" class="{{ default_theme }} sidebar-visible" dir="{{ text_direction }}">
+    <head>
+        <!-- Book generated using mdBook -->
+        <meta charset="UTF-8">
+        <title>{{ title }}</title>
+        {{#if is_print }}
+        <meta name="robots" content="noindex">
+        {{/if}}
+        {{#if base_url}}
+        <base href="{{ base_url }}">
+        {{/if}}
+
+
+        <!-- Custom HTML head -->
+        {{> head}}
+
+        <meta name="description" content="{{ description }}">
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+        <meta name="theme-color" content="#ffffff">
+
+        {{#if favicon_svg}}
+        <link rel="icon" href="{{ resource "favicon.svg" }}">
+        {{/if}}
+        {{#if favicon_png}}
+        <link rel="shortcut icon" href="{{ resource "favicon.png" }}">
+        {{/if}}
+        <link rel="stylesheet" href="{{ resource "css/variables.css" }}">
+        <link rel="stylesheet" href="{{ resource "css/general.css" }}">
+        <link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
+        {{#if print_enable}}
+        <link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
+        {{/if}}
+
+        <!-- Fonts -->
+        <link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
+        {{#if copy_fonts}}
+        <link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
+        {{/if}}
+
+        <!-- Highlight.js Stylesheets -->
+        <link rel="stylesheet" id="highlight-css" href="{{ resource "highlight.css" }}">
+        <link rel="stylesheet" id="tomorrow-night-css" href="{{ resource "tomorrow-night.css" }}">
+        <link rel="stylesheet" id="ayu-highlight-css" href="{{ resource "ayu-highlight.css" }}">
+
+        <!-- Custom theme stylesheets -->
+        {{#each additional_css}}
+        <link rel="stylesheet" href="{{ resource this }}">
+        {{/each}}
+
+        {{#if mathjax_support}}
+        <!-- MathJax -->
+        <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
+        {{/if}}
+
+        <!-- Provide site root to javascript -->
+        <script>
+            const path_to_root = "{{ path_to_root }}";
+            const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
+        </script>
+        <!-- Start loading toc.js asap -->
+        <script src="{{ resource "toc.js" }}"></script>
+    </head>
+    <body>
+    <div id="body-container">
+        <!-- Work around some values being stored in localStorage wrapped in quotes -->
+        <script>
+            try {
+                let theme = localStorage.getItem('mdbook-theme');
+                let sidebar = localStorage.getItem('mdbook-sidebar');
+
+                if (theme.startsWith('"') && theme.endsWith('"')) {
+                    localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
+                }
+
+                if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
+                    localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
+                }
+            } catch (e) { }
+        </script>
+
+        <!-- Set the theme before any content is loaded, prevents flash -->
+        <script>
+            let theme;
+            try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
+            if (theme === null || theme === undefined) { theme = default_theme; }
+            const html = document.documentElement;
+            html.classList.remove('{{ default_theme }}')
+            html.classList.add(theme);
+            html.classList.add("js");
+        </script>
+
+        <input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
+
+        <!-- Hide / unhide sidebar before it is displayed -->
+        <script>
+            let sidebar = null;
+            const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
+            if (document.body.clientWidth >= 1080) {
+                try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
+                sidebar = sidebar || 'visible';
+            } else {
+                sidebar = 'hidden';
+            }
+            sidebar_toggle.checked = sidebar === 'visible';
+            html.classList.remove('sidebar-visible');
+            html.classList.add("sidebar-" + sidebar);
+        </script>
+
+        <nav id="sidebar" class="sidebar" aria-label="Table of contents">
+            <!-- populated by js -->
+            <mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
+            <noscript>
+                <iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
+            </noscript>
+            <div id="sidebar-resize-handle" class="sidebar-resize-handle">
+                <div class="sidebar-resize-indicator"></div>
+            </div>
+        </nav>
+
+        <div id="page-wrapper" class="page-wrapper">
+
+            <div class="page">
+                {{> header}}
+                <div id="menu-bar-hover-placeholder"></div>
+                <div id="menu-bar" class="menu-bar sticky">
+                    <div class="left-buttons">
+                        <label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
+                            <i class="fa fa-bars"></i>
+                        </label>
+                        <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
+                            <i class="fa fa-paint-brush"></i>
+                        </button>
+                        <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
+                            <li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
+                            <li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
+                            <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
+                            <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
+                            <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
+                        </ul>
+                        <button id="lang-toggle" class="icon-button" type="button" title="Change language" aria-label="Change language" aria-haspopup="true" aria-expanded="false" aria-controls="lang-list">
+                            <i class="fa fa-language"></i>
+                        </button>
+                        <ul id="lang-list" class="theme-popup" aria-label="Languages" role="menu">
+                          <li role="none"><button role="menuitem" class="theme" data-lang="en">English</button></li>
+                          <li role="none"><button role="menuitem" class="theme" data-lang="zh-TW">中文</button></li>
+                        </ul>
+                        {{#if search_enabled}}
+                        <button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
+                            <i class="fa fa-search"></i>
+                        </button>
+                        {{/if}}
+                    </div>
+
+                    <h1 class="menu-title">{{ book_title }}</h1>
+
+                    <div class="right-buttons">
+                        {{#if print_enable}}
+                        <a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
+                            <i id="print-button" class="fa fa-print"></i>
+                        </a>
+                        {{/if}}
+                        {{#if git_repository_url}}
+                        <a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
+                            <i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
+                        </a>
+                        {{/if}}
+                        {{#if git_repository_edit_url}}
+                        <a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
+                            <i id="git-edit-button" class="fa fa-edit"></i>
+                        </a>
+                        {{/if}}
+
+                    </div>
+                </div>
+
+                {{#if search_enabled}}
+                <div id="search-wrapper" class="hidden">
+                    <form id="searchbar-outer" class="searchbar-outer">
+                        <input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
+                    </form>
+                    <div id="searchresults-outer" class="searchresults-outer hidden">
+                        <div id="searchresults-header" class="searchresults-header"></div>
+                        <ul id="searchresults">
+                        </ul>
+                    </div>
+                </div>
+                {{/if}}
+
+                <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
+                <script>
+                    document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
+                    document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
+                    Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
+                        link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
+                    });
+                </script>
+
+                <div id="content" class="content">
+                    <main>
+                        {{{ content }}}
+                    </main>
+
+                    <nav class="nav-wrapper" aria-label="Page navigation">
+                        <!-- Mobile navigation buttons -->
+                        {{#previous}}
+                            <a rel="prev" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
+                                <i class="fa fa-angle-left"></i>
+                            </a>
+                        {{/previous}}
+
+                        {{#next}}
+                            <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
+                                <i class="fa fa-angle-right"></i>
+                            </a>
+                        {{/next}}
+
+                        <div style="clear: both"></div>
+                    </nav>
+                </div>
+            </div>
+
+            <nav class="nav-wide-wrapper" aria-label="Page navigation">
+                {{#previous}}
+                    <a rel="prev" href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
+                        <i class="fa fa-angle-left"></i>
+                    </a>
+                {{/previous}}
+
+                {{#next}}
+                    <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
+                        <i class="fa fa-angle-right"></i>
+                    </a>
+                {{/next}}
+            </nav>
+
+        </div>
+
+        {{#if live_reload_endpoint}}
+        <!-- Livereload script (if served using the cli tool) -->
+        <script>
+            const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
+            const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
+            const socket = new WebSocket(wsAddress);
+            socket.onmessage = function (event) {
+                if (event.data === "reload") {
+                    socket.close();
+                    location.reload();
+                }
+            };
+
+            window.onbeforeunload = function() {
+                socket.close();
+            }
+        </script>
+        {{/if}}
+
+        {{#if google_analytics}}
+        <!-- Google Analytics Tag -->
+        <script>
+            const localAddrs = ["localhost", "127.0.0.1", ""];
+
+            // make sure we don't activate google analytics if the developer is
+            // inspecting the book locally...
+            if (localAddrs.indexOf(document.location.hostname) === -1) {
+                (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+                (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+                m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+                })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+
+                ga('create', '{{google_analytics}}', 'auto');
+                ga('send', 'pageview');
+            }
+        </script>
+        {{/if}}
+
+        {{#if playground_line_numbers}}
+        <script>
+            window.playground_line_numbers = true;
+        </script>
+        {{/if}}
+
+        {{#if playground_copyable}}
+        <script>
+            window.playground_copyable = true;
+        </script>
+        {{/if}}
+
+        {{#if playground_js}}
+        <script src="{{ resource "ace.js" }}"></script>
+        <script src="{{ resource "editor.js" }}"></script>
+        <script src="{{ resource "mode-rust.js" }}"></script>
+        <script src="{{ resource "theme-dawn.js" }}"></script>
+        <script src="{{ resource "theme-tomorrow_night.js" }}"></script>
+        {{/if}}
+
+        {{#if search_js}}
+        <script src="{{ resource "elasticlunr.min.js" }}"></script>
+        <script src="{{ resource "mark.min.js" }}"></script>
+        <script src="{{ resource "searcher.js" }}"></script>
+        {{/if}}
+
+        <script src="{{ resource "clipboard.min.js" }}"></script>
+        <script src="{{ resource "highlight.js" }}"></script>
+        <script src="{{ resource "book.js" }}"></script>
+
+        <!-- Custom JS scripts -->
+        {{#each additional_js}}
+        <script src="{{ resource this}}"></script>
+        {{/each}}
+
+        {{#if is_print}}
+        {{#if mathjax_support}}
+        <script>
+        window.addEventListener('load', function() {
+            MathJax.Hub.Register.StartupHook('End', function() {
+                window.setTimeout(window.print, 100);
+            });
+        });
+        </script>
+        {{else}}
+        <script>
+        window.addEventListener('load', function() {
+            window.setTimeout(window.print, 100);
+        });
+        </script>
+        {{/if}}
+        {{/if}}
+
+    </div>
+    </body>
+</html>
diff --git a/guide/theme/lang_toggle.css b/guide/theme/lang_toggle.css
new file mode 100644
index 0000000..72fc3be
--- /dev/null
+++ b/guide/theme/lang_toggle.css
@@ -0,0 +1,13 @@
+/* 1) Make .left-buttons positioned so its child popups can anchor to it */
+.left-buttons {
+  position: relative;
+}
+
+/* 2) Specifically override #lang-list to appear at the right edge under the button */
+.left-buttons #lang-list.theme-popup {
+  left: auto;                  /* remove the default left: 10px */
+  right: 0;                    /* pin to the right edge of .left-buttons */
+  top: calc(var(--menu-bar-height) + 4px); 
+  width: auto;                 /* don’t stretch across the screen */
+  white-space: nowrap;         /* keep the list from wrapping into multiple lines */
+}
diff --git a/guide/theme/lang_toggle.js b/guide/theme/lang_toggle.js
new file mode 100644
index 0000000..5ef55f6
--- /dev/null
+++ b/guide/theme/lang_toggle.js
@@ -0,0 +1,138 @@
+'use strict';
+
+(function languages() {
+  const langToggleButton = document.getElementById('lang-toggle');
+  const langPopup = document.getElementById('lang-list');
+
+  if (!langToggleButton || !langPopup) {
+      return; // Safety check: if they're missing from the page, do nothing
+  }
+
+  function showLangs() {
+      langPopup.style.display = 'block';
+      langToggleButton.setAttribute('aria-expanded', 'true');
+  }
+
+  function hideLangs() {
+      langPopup.style.display = 'none';
+      langToggleButton.setAttribute('aria-expanded', 'false');
+      langToggleButton.focus();
+  }
+
+  langToggleButton.addEventListener('click', function() {
+      if (langPopup.style.display === 'block') {
+          hideLangs();
+      } else {
+          showLangs();
+      }
+  });
+
+  document.addEventListener('click', function (e) {
+    if (e.target && e.target.matches('button[data-lang]')) {
+      const chosenLang = e.target.getAttribute('data-lang');
+      const supportedLangs = ['en', 'zh-TW']; // Add translated languages here
+  
+      let currentPath = window.location.pathname;
+  
+      // Find "book/" in the path
+      const bookAnchor = 'book/';
+      const idx = currentPath.indexOf(bookAnchor);
+  
+      if (idx === -1) {
+        // Fallback: If "book/" isn’t in the path
+        // Just go to the top-level file in the chosen language.
+        if (chosenLang === 'en') 
+          window.location.href = 'index.html'; 
+        else 
+          window.location.href = chosenLang + '/index.html';
+
+        return;
+      }
+  
+      // Everything up to (and including) "book/"
+      // e.g. "/C:/Users/.../guide/book/" 
+      const base = currentPath.substring(0, idx + bookAnchor.length);
+  
+      // The rest "after book/" part
+      // e.g. "index.html" or "zh-TW/getting_started/foo.html"
+      let after = currentPath.substring(idx + bookAnchor.length);
+  
+      // Split into segments and remove any leading lang if it’s known
+      // e.g. ["index.html"] or ["en", "getting_started", "foo.html"]
+      let segments = after.split('/').filter(s => s.length > 0);
+  
+      if (segments.length > 0 && supportedLangs.includes(segments[0])) {
+        // remove the first segment if it’s a supported lang
+        // e.g. now ["getting_started", "foo.html"]
+        segments.shift(); 
+      }
+  
+      // Insert the chosen language as the first path segment
+      // Also, English has no prefix
+      let newPath;
+      if (chosenLang === 'en') {
+        // e.g. /C:/Users/.../guide/book/getting_started/foo.html
+        newPath = base + segments.join('/');
+      } else {
+        // e.g. /C:/Users/.../guide/book/zh-TW/getting_started/foo.html
+        newPath = base + chosenLang + '/' + segments.join('/');
+      }
+  
+      window.location.href = newPath;
+    }
+  
+    if (
+      langPopup.style.display === 'block' &&
+      !langToggleButton.contains(e.target) &&
+      !langPopup.contains(e.target)
+    ) {
+      hideLangs();
+    }
+  });
+
+  // Also hide if focus goes elsewhere
+  langPopup.addEventListener('focusout', function(e) {
+      // e.relatedTarget can be null in some browsers
+      if (!!e.relatedTarget &&
+          !langToggleButton.contains(e.relatedTarget) &&
+          !langPopup.contains(e.relatedTarget)) {
+          hideLangs();
+      }
+  });
+
+  // Optional: Add keyboard navigation (like theme popup)
+  document.addEventListener('keydown', function(e) {
+      if (langPopup.style.display !== 'block') return;
+      if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
+
+      let li;
+      switch (e.key) {
+          case 'Escape':
+              e.preventDefault();
+              hideLangs();
+              break;
+          case 'ArrowUp':
+              e.preventDefault();
+              li = document.activeElement.parentElement;
+              if (li && li.previousElementSibling) {
+                  li.previousElementSibling.querySelector('a, button').focus();
+              }
+              break;
+          case 'ArrowDown':
+              e.preventDefault();
+              li = document.activeElement.parentElement;
+              if (li && li.nextElementSibling) {
+                  li.nextElementSibling.querySelector('a, button').focus();
+              }
+              break;
+          case 'Home':
+              e.preventDefault();
+              langPopup.querySelector('li:first-child a, li:first-child button').focus();
+              break;
+          case 'End':
+              e.preventDefault();
+              langPopup.querySelector('li:last-child a, li:last-child button').focus();
+              break;
+      }
+  });
+})();
diff --git a/guide/translations/.gitignore b/guide/translations/.gitignore
new file mode 100644
index 0000000..d1e87fb
--- /dev/null
+++ b/guide/translations/.gitignore
@@ -0,0 +1 @@
+theme
\ No newline at end of file
diff --git a/guide/translations/zh-TW/book.toml b/guide/translations/zh-TW/book.toml
new file mode 100644
index 0000000..2e0702a
--- /dev/null
+++ b/guide/translations/zh-TW/book.toml
@@ -0,0 +1,10 @@
+[book]
+authors = ["Karn Kaul", "Mes"]
+language = "zh-TW"
+src = "src"
+title = "Learn Vulkan"
+
+[output.html]
+theme = "../theme"
+additional-js = ["../theme/lang_toggle.js"]
+additional-css = ["../theme/lang_toggle.css"]
\ No newline at end of file
diff --git a/guide/translations/zh-TW/src/README.md b/guide/translations/zh-TW/src/README.md
new file mode 100644
index 0000000..edeb023
--- /dev/null
+++ b/guide/translations/zh-TW/src/README.md
@@ -0,0 +1,33 @@
+# (中文 WIP) Intro
+
+Vulkan is known for being explicit and verbose. But the _required_ verbosity has steadily reduced with each successive version, its new features, and previous extensions being absorbed into the core API. Similarly, RAII has been a pillar of C++ since its inception, yet most Vulkan guides do not utilize it, instead choosing to "extend" the explicitness by manually cleaning up resources.
+
+To fill that gap, this guide has the following goals:
+
+- Leverage modern C++, VulkanHPP, and Vulkan 1.3 features
+- Focus on keeping it simple and straightforward, _not_ on performance
+- Develop a basic but dynamic rendering foundation
+
+To reiterate, the focus is _not on performance_, it is on a quick introduction to the current standard multi-platform graphics API while utilizing the modern paradigms and tools (at the time of writing). Even disregarding potential performance gains, Vulkan has a better and modern design and ecosystem than OpenGL, eg: there is no global state machine, parameters are passed by filling structs with meaningful member variable names, multi-threading is largely trivial (yes, it is actually easier to do on Vulkan than OpenGL), there are a comprehensive set of validation layers to catch misuse which can be enabled without _any_ changes to application code, etc.
+
+## Target Audience
+
+The guide is for you if you:
+
+- Understand the principles of modern C++ and its usage
+- Have created C++ projects using third-party libraries
+- Are somewhat familiar with graphics
+    - Having done OpenGL tutorials would be ideal
+    - Experience with frameworks like SFML / SDL is great
+- Don't mind if all the information you need isn't monolithically in one place (ie, this guide)
+
+Some examples of what this guide _does not_ focus on:
+
+- GPU-driven rendering
+- Real-time graphics from ground-up
+- Considerations for tiled GPUs (eg mobile devices / Android)
+
+## Source
+
+The source code for the project (as well as this guide) is located in [this repository](https://github.com/cpp-gamedev/learn-vulkan). A `section/*` branch intends to reflect the state of the code at the end of a particular section of the guide. Bugfixes / changes are generally backported, but there may be some divergence from the current state of the code (ie, in `main`). The source of the guide itself is only up-to-date on `main`, changes are not backported.
+
diff --git a/guide/translations/zh-TW/src/SUMMARY.md b/guide/translations/zh-TW/src/SUMMARY.md
new file mode 100644
index 0000000..7fc1a4b
--- /dev/null
+++ b/guide/translations/zh-TW/src/SUMMARY.md
@@ -0,0 +1,10 @@
+# (中文 WIP) Summary
+
+[(中文 WIP) Introduction](README.md)
+
+# (中文 WIP) Basics
+
+- [(中文 WIP) Getting Started](getting_started/README.md)
+  - [(中文 WIP) Project Layout](getting_started/project_layout.md)
+  - [(中文 WIP) Validation Layers](getting_started/validation_layers.md)
+  - [(中文 WIP) class App](getting_started/class_app.md)
diff --git a/guide/translations/zh-TW/src/getting_started/README.md b/guide/translations/zh-TW/src/getting_started/README.md
new file mode 100644
index 0000000..53b33c2
--- /dev/null
+++ b/guide/translations/zh-TW/src/getting_started/README.md
@@ -0,0 +1,34 @@
+# (中文 WIP) Getting Started
+
+Vulkan is platform agnostic, which is one of the main reasons for its verbosity: it has to account for a wide range of implementations in its API. We shall be constraining our approach to Windows and Linux (x64 or aarch64), and focusing on discrete GPUs, enabing us to sidestep quite a bit of that verbosity. Vulkan 1.3 is widely supported by the target desktop platforms and reasonably recent graphics cards.
+
+> This doesn't mean that eg an integrated graphics chip will not be supported, it will just not be particularly designed/optimized for.
+
+## Technical Requirements
+
+1. Vulkan 1.3+ capable GPU and loader
+1. [Vulkan 1.3+ SDK](https://vulkan.lunarg.com/sdk/home)
+    1. This is required for validation layers, a critical component/tool to use when developing Vulkan applications. The project itself does not use the SDK.
+    1. Always using the latest SDK is recommended (1.4.x at the time of writing).
+1. Desktop operating system that natively supports Vulkan
+    1. Windows and/or Linux (distros that use repos with recent packages) is recommended.
+    1. MacOS does _not_ natively support Vulkan. It _can_ be used through MoltenVk, but at the time of writing MoltenVk does not fully support Vulkan 1.3, so if you decide to take this route, you may face some roadblocks.
+1. C++23 compiler and standard library
+    1. GCC14+, Clang18+, and/or latest MSVC are recommended. MinGW/MSYS is _not_ recommended.
+    1. Using C++20 with replacements for C++23 specific features is possible. Eg replace `std::print()` with `fmt::print()`, add `()` to lambdas, etc.
+1. CMake 3.24+
+
+## Overview
+
+While support for C++ modules is steadily growing, tooling is not yet ready on all platforms/IDEs we want to target, so we will unfortuntely still be using headers. This might change in the near future, followed by a refactor of this guide.
+
+The project uses a "Build the World" approach, enabling usage of sanitizers, reproducible builds on any supported platform, and requiring minimum pre-installed things on target machines. Feel free to use pre-built binaries instead, it doesn't change anything about how you would use Vulkan.
+
+## Dependencies
+
+1. [GLFW](https://github.com/glfw/glfw) for windowing, input, and Surface creation
+1. [VulkanHPP](https://github.com/KhronosGroup/Vulkan-Hpp) (via [Vulkan-Headers](https://github.com/KhronosGroup/Vulkan-Headers)) for interacting with Vulkan
+    1. While Vulkan is a C API, it offers an official C++ wrapper library with many quality-of-life features. This guide almost exclusively uses that, except at the boundaries of other C libraries that themselves use the C API (eg GLFW and VMA).
+1. [Vulkan Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/) for dealing with Vulkan memory heaps
+1. [GLM](https://github.com/g-truc/glm) for GLSL-like linear algebra in C++
+1. [Dear ImGui](https://github.com/ocornut/imgui) for UI
diff --git a/guide/translations/zh-TW/src/getting_started/class_app.md b/guide/translations/zh-TW/src/getting_started/class_app.md
new file mode 100644
index 0000000..112ea26
--- /dev/null
+++ b/guide/translations/zh-TW/src/getting_started/class_app.md
@@ -0,0 +1,39 @@
+# (中文 WIP) Application
+
+`class App` will serve as the owner and driver of the entire application. While there will only be one instance, using a class enables us to leverage RAII and destroy all its resources automatically and in the correct order, and avoids the need for globals.
+
+```cpp
+// app.hpp
+namespace lvk {
+class App {
+ public:
+  void run();
+};
+} // namespace lvk
+
+// app.cpp
+namespace lvk {
+void App::run() {
+  // TODO
+}
+} // namespace lvk
+```
+
+## Main
+
+`main.cpp` will not do much: it's mainly responsible for transferring control to the actual entry point, and catching fatal exceptions.
+
+```cpp
+// main.cpp
+auto main() -> int {
+  try {
+    lvk::App{}.run();
+  } catch (std::exception const& e) {
+    std::println(stderr, "PANIC: {}", e.what());
+    return EXIT_FAILURE;
+  } catch (...) {
+    std::println("PANIC!");
+    return EXIT_FAILURE;
+  }
+}
+```
diff --git a/guide/translations/zh-TW/src/getting_started/high_level_loader.png b/guide/translations/zh-TW/src/getting_started/high_level_loader.png
new file mode 100644
index 0000000..cbfd3b6
Binary files /dev/null and b/guide/translations/zh-TW/src/getting_started/high_level_loader.png differ
diff --git a/guide/translations/zh-TW/src/getting_started/project_layout.md b/guide/translations/zh-TW/src/getting_started/project_layout.md
new file mode 100644
index 0000000..a993c3e
--- /dev/null
+++ b/guide/translations/zh-TW/src/getting_started/project_layout.md
@@ -0,0 +1,22 @@
+# (中文 WIP) Project Layout
+
+This page describes the layout used by the code in this guide. Everything here is just an opinionated option used by the guide, and is not related to Vulkan usage.
+
+External dependencies are stuffed into a zip file that's decompressed by CMake during the configure stage. Using FetchContent is a viable alternative.
+
+`Ninja Multi-Config` is the assumed generator used, regardless of OS/compiler. This is set up in a `CMakePresets.json` file in the project root. Additional custom presets can be added via `CMakeUserPresets.json`.
+
+> On Windows, Visual Studio CMake Mode uses this generator and automatically loads presets. With Visual Studio Code, the CMake Tools extension automatically uses presets. For other IDEs, refer to their documentation on using CMake presets.
+
+**Filesystem**
+
+```
+.
+|-- CMakeLists.txt         <== executable target
+|-- CMakePresets.json
+|-- [other project files]
+|-- ext/
+│   |-- CMakeLists.txt     <== external dependencies target
+|-- src/
+    |-- [sources and headers]
+```
\ No newline at end of file
diff --git a/guide/translations/zh-TW/src/getting_started/validation_layers.md b/guide/translations/zh-TW/src/getting_started/validation_layers.md
new file mode 100644
index 0000000..42494a8
--- /dev/null
+++ b/guide/translations/zh-TW/src/getting_started/validation_layers.md
@@ -0,0 +1,9 @@
+# (中文 WIP) Validation Layers
+
+The area of Vulkan that apps interact with: the loader, is very powerful and flexible. Read more about it [here](https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderInterfaceArchitecture.md). Its design enables it to chain API calls through configurable **layers**, eg for overlays, and most importantly for us: [Validation Layers](https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/main/docs/README.md).
+
+![Vulkan Loader](high_level_loader.png)
+
+As [suggested](https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/main/docs/khronos_validation_layer.md#vkconfig) by the Khronos Group, we recommend using [Vulkan Configurator (GUI)](https://github.com/LunarG/VulkanTools/tree/main/vkconfig_gui) for validation layers. This application is shipped with the Vulkan SDK, just keep it running while developing Vulkan applications, and ensure it is setup to inject validation layers into all detected applications, with Synchronization Validation enabled. This approach provides a lot of flexibility at runtime, including the ability to have VkConfig break the debugger on encountering an error, and also eliminates the need for validation layer specific code in the applications.
+
+> Note: modify your development (or desktop) environment's `PATH` (or use `LD_LIBRARY_PATH` on supported systems) to make sure the SDK's binaries are visible first.
\ No newline at end of file


Reviews:
user: login: karnkaul
body: Nice! Is there a way to not remove `book.toml` from the source tree? 
When I get some time I can update the CMake code to use [script mode](https://cmake.org/cmake/help/latest/manual/cmake-language.7.html#scripts). Unless you want to try it yourself.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T18:22:10Z
user: login: Mes0903
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T05:27:28Z
user: login: Mes0903
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T05:27:48Z
user: login: karnkaul
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-28T16:23:06Z
user: login: karnkaul
body: Excellent work, congratulations and thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T16:47:17Z

Review comments:
path: guide/CMakeLists.txt
user: login: karnkaul
body: This is pretty cool! There's also CMake script mode, which I think this suits better than being a full `CMakeLists.txt` file, which is conventionally used to define build targets. BTW, here's the current [deploy script](https://github.com/cpp-gamedev/learn-vulkan/blob/main/.github/workflows/deploy.yml) for reference.
created_at: 2025-03-27T18:15:47Z
author_association: MEMBER
diff_hunk: @@ -1 +1,3 @@
 book
+build/*
+book.toml
path: guide/.gitignore
user: login: karnkaul
body: I'm not sure if removing `book.toml` from source is a good idea, but maybe there's no other clean way if this is now generated by a script...
created_at: 2025-03-27T18:17:40Z
author_association: MEMBER
path: guide/book-template.config
user: login: karnkaul
body: What does this file do?
created_at: 2025-03-27T18:17:55Z
author_association: MEMBER
path: guide/book-template.config
user: login: Mes0903
body: The cmake script will use the `book-template.config` to generate the corresponding `book.toml` for each language. Take `en` as an example, the cmake script will replaced the `@AUTHORS@` and `@LANGUAGE@` in the `book-template.config` by the hardcode setting in cmake script:

```
MakeConfig("en" "Karn Kaul")
```

Now, because the `book.toml` was generated by the script, so I add it into the `.gitignore`. If we want to reserve the `book.toml`, maybe we could simply add `book.toml` for each language manually. Then the script will just doing the build work but not generate any file.
created_at: 2025-03-28T05:27:28Z
author_association: MEMBER
path: guide/CMakeLists.txt
user: login: Mes0903
body: I think it's better to update it in this commit, I will try it.
created_at: 2025-03-28T05:27:48Z
author_association: MEMBER
path: guide/build-books.cmake
user: login: karnkaul
body: Nitpick: filenames are in `snake_case`, better to remain consistent. `build_books.cmake`
created_at: 2025-03-28T06:31:38Z
author_association: MEMBER
path: guide/theme/lang-toggle.css
user: login: karnkaul
body: We should stick to `snake_case` for filenames: `lang_toggle.css`, `lang_toggle.js`. Maybe `zh-TW` can be an exception if it's standard/convention.
created_at: 2025-03-28T16:21:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1 @@
+theme
path: guide/translations/.gitignore
user: login: karnkaul
body: This is generated by `guide/build.cmake`?
created_at: 2025-03-28T16:22:34Z
author_association: MEMBER

PR comments:
user: login: Mes0903
created_at: 2025-03-27T18:05:38Z
body: I have no confidence to write the cmake script very well, so I may need some advice here. 

For now, I build the document simply via

```
mkdir build && cd build
cmake ..
```

I dont think this is the correct way but I have no idea what's the correct method.
author_association: MEMBER
user: login: Mes0903
created_at: 2025-03-28T12:22:14Z
body: The code now can be tested by

- `cmake -P build.cmake` : `mdbook build` for all language
- `cmake -DSERVE=ON build.cmake` : this will build all translations that registered in the script, and move it into the folder that `mdbook serve` generate, which usually called `book`

It should have no problem for the first one, but for the second command, there is some topic that we can discuss

Take zh-TW as example, the processing is generally as following:

- Build zh-TW document into `.tmp-output/zh-TW`
- Start `mdbook serve` for English
- Wait for `book/` folder to be created by mdbook serve
- Then move `.tmp-output/zh-TW` → `book/zh-TW`
- Clean up `.tmp-output`

Since `mdbook serve` will block the shell, so I open an new shell to run the command:

```
if(WIN32)
  execute_process(
    COMMAND powershell -Command "Start-Process mdbook -ArgumentList 'serve','-d','${CMAKE_CURRENT_SOURCE_DIR}/book'"
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
  )
else()
  execute_process(
    COMMAND bash -c "mdbook serve -d \"${CMAKE_CURRENT_SOURCE_DIR}/book\" &"
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
  )  
endif()
```

For now, this work successfully on windows, it come out an new powershell showing the info of `mdbook serve` command, and the original shell keep on the translation folder movement work. But on linux, this didnt work as expected, it didnt open an new shell

The command `mdbook serve` need to take some time to generate the `book` folder, For now, I use the naive way ー sleep, to make sure the translation folder movement would start after the `book` folder was generated:

```
# Move the zh-TW output folder into book/
message(STATUS "Moving the translation folder to final location...")
execute_process(COMMAND ${CMAKE_COMMAND} -E sleep 2)

# Move the translation folder to the book folder
file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/book")
file(RENAME
  "${CMAKE_CURRENT_SOURCE_DIR}/.tmp-output/zh-TW"
  "${CMAKE_CURRENT_SOURCE_DIR}/book/zh-TW"
)
```

I dont think this is an good way, it should be done as sth like await mechanism, is there sth similar in cmake script?
author_association: MEMBER
user: login: Mes0903
created_at: 2025-03-28T17:04:20Z
body: - After our discussion, we decided not to include the `mdbook serve` flag in the CMake script. If you want to use the `mdbook serve` feature, as mentioned in the commit message, you need to manually build all translations first. Then, after the command `mdbook serve` creates the book folder, move those translation folders into it.

- If you want to add a new language, you must:
  - Add the target language to the lang-list in index.hbs.
  - Modify the supportedLangs array in lang_toggle.js.
  - Finally, register it in build.cmake.

- The filenames of translated documents must match the original (English) filenames. The folder structure for translations looks like this:
	```
	\---zh-TW
	    |   book.toml
	    |
	    \---src
	        |   README.md
	        |   SUMMARY.md
	        |
	        \---(All other folders and filenames must match the primary English folder(`src`) structure)
	```


author_association: MEMBER