retypeset-odyssey 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +168 -0
  3. package/astro.config.ts +18 -0
  4. package/default-config.yaml +136 -0
  5. package/discover-collections.ts +160 -0
  6. package/integration.ts +394 -0
  7. package/package.json +105 -0
  8. package/patches/@qwik.dev__partytown@0.11.2.patch +98 -0
  9. package/public/_redirects +819 -0
  10. package/public/feeds/atom-style.xsl +105 -0
  11. package/public/feeds/rss-style.xsl +105 -0
  12. package/public/fonts/EarlySummer-VF-Split/00785494587e3487ac63a0e7e4fa30f0.woff2 +0 -0
  13. package/public/fonts/EarlySummer-VF-Split/08e5d941a4c76fad7b68e7a937ebb21f.woff2 +0 -0
  14. package/public/fonts/EarlySummer-VF-Split/1268e5072156188d601f1eeb4473655d.woff2 +0 -0
  15. package/public/fonts/EarlySummer-VF-Split/12a385475353c815d7a5add53ee51e37.woff2 +0 -0
  16. package/public/fonts/EarlySummer-VF-Split/12b11ca08223c65a21fc731d59dcfc11.woff2 +0 -0
  17. package/public/fonts/EarlySummer-VF-Split/16d6676d3cb645c520ee6df8a1f89afd.woff2 +0 -0
  18. package/public/fonts/EarlySummer-VF-Split/2912a75ffef95e7a5ae9e2b2311ad61d.woff2 +0 -0
  19. package/public/fonts/EarlySummer-VF-Split/298d96ea561e419a4104bc9fc18499ce.woff2 +0 -0
  20. package/public/fonts/EarlySummer-VF-Split/2a2c71acc17ec39f6780835899e53096.woff2 +0 -0
  21. package/public/fonts/EarlySummer-VF-Split/2a7e2d0e59d3f638074c50fab39fdef1.woff2 +0 -0
  22. package/public/fonts/EarlySummer-VF-Split/36931fc4370e1670ed76af5d3feccba2.woff2 +0 -0
  23. package/public/fonts/EarlySummer-VF-Split/3a68fdc792e4a9e0399a04e32d0cc2e3.woff2 +0 -0
  24. package/public/fonts/EarlySummer-VF-Split/4054d6a4d6b37719b51e0f71da6e7cd9.woff2 +0 -0
  25. package/public/fonts/EarlySummer-VF-Split/429cb25f825c3cbde6bfac5b36ae9675.woff2 +0 -0
  26. package/public/fonts/EarlySummer-VF-Split/42a9efc11298368ecdc1b85ab46f0b4f.woff2 +0 -0
  27. package/public/fonts/EarlySummer-VF-Split/432018d2bdc9df92a7662056eb2b1261.woff2 +0 -0
  28. package/public/fonts/EarlySummer-VF-Split/44a6fb782f2a01560faa0f95248b60ef.woff2 +0 -0
  29. package/public/fonts/EarlySummer-VF-Split/45367b060e8ba0aa2507e6b91b86620b.woff2 +0 -0
  30. package/public/fonts/EarlySummer-VF-Split/571db7564bda7c1a93542881b8976f4b.woff2 +0 -0
  31. package/public/fonts/EarlySummer-VF-Split/58d55eeef4cf455e86a1142b1f3110d3.woff2 +0 -0
  32. package/public/fonts/EarlySummer-VF-Split/59ea41e77309160a0f63cdc76a010202.woff2 +0 -0
  33. package/public/fonts/EarlySummer-VF-Split/5d19d9174e568db4755981aa2e4ab380.woff2 +0 -0
  34. package/public/fonts/EarlySummer-VF-Split/5e811eb3b4175ee93d7ec000bf4631c2.woff2 +0 -0
  35. package/public/fonts/EarlySummer-VF-Split/6268e0cd5d66d6fe05b331f259e7b9e4.woff2 +0 -0
  36. package/public/fonts/EarlySummer-VF-Split/6549844aa3d833ca06a68a8e839db465.woff2 +0 -0
  37. package/public/fonts/EarlySummer-VF-Split/714b459658a7321ceeb1e1386ce165c2.woff2 +0 -0
  38. package/public/fonts/EarlySummer-VF-Split/7511d97a469915013683eae06cb21cd9.woff2 +0 -0
  39. package/public/fonts/EarlySummer-VF-Split/7784b4ebe543d13f62f6f6e05beb0b2e.woff2 +0 -0
  40. package/public/fonts/EarlySummer-VF-Split/77c9bea70b3c6ab24e1497d5468c825b.woff2 +0 -0
  41. package/public/fonts/EarlySummer-VF-Split/789ebea9e81df623e930b86de98fbfab.woff2 +0 -0
  42. package/public/fonts/EarlySummer-VF-Split/885bb7ab0717e8a47fc17f953adcdbf1.woff2 +0 -0
  43. package/public/fonts/EarlySummer-VF-Split/896c58aff69a9a857764cee0663bc56d.woff2 +0 -0
  44. package/public/fonts/EarlySummer-VF-Split/8fb6fc01c59d1e3ad1910b58dec7f5e7.woff2 +0 -0
  45. package/public/fonts/EarlySummer-VF-Split/95be5462b91b9a0458797cdc89d94cb5.woff2 +0 -0
  46. package/public/fonts/EarlySummer-VF-Split/9a5b2724f983ca0fc0d5ff8d10c41396.woff2 +0 -0
  47. package/public/fonts/EarlySummer-VF-Split/9ffe17f9c0e4cc4356cb3f08ffdb9c6d.woff2 +0 -0
  48. package/public/fonts/EarlySummer-VF-Split/EarlySummer-VF-Subset.woff2 +0 -0
  49. package/public/fonts/EarlySummer-VF-Split/EarlySummerSerif License.txt +91 -0
  50. package/public/fonts/EarlySummer-VF-Split/a097ef49be62cd2565aca45600e1e3ac.woff2 +0 -0
  51. package/public/fonts/EarlySummer-VF-Split/a17a1ae6063088e5b3a48c06b816929a.woff2 +0 -0
  52. package/public/fonts/EarlySummer-VF-Split/a83fdcfc5ecf2f6996704b0c02758689.woff2 +0 -0
  53. package/public/fonts/EarlySummer-VF-Split/a8cf15ff9b71e59407d8406866ff6f99.woff2 +0 -0
  54. package/public/fonts/EarlySummer-VF-Split/af530ed51dd519e4456f8a5e259e908b.woff2 +0 -0
  55. package/public/fonts/EarlySummer-VF-Split/b195a8924915deec4aa9c3ec777cc93f.woff2 +0 -0
  56. package/public/fonts/EarlySummer-VF-Split/b4b6bb5df9239dd67b52ca858fd2a506.woff2 +0 -0
  57. package/public/fonts/EarlySummer-VF-Split/b7592e1e027923f19e0e55dfdac69668.woff2 +0 -0
  58. package/public/fonts/EarlySummer-VF-Split/b965859f69d8ccceaf0e2d6292afbcfb.woff2 +0 -0
  59. package/public/fonts/EarlySummer-VF-Split/bbe9333f1ff242bd96ecb23ff9e723b1.woff2 +0 -0
  60. package/public/fonts/EarlySummer-VF-Split/be758580e295339ea98f0240b9869f24.woff2 +0 -0
  61. package/public/fonts/EarlySummer-VF-Split/c07099e1d025617f6d40966986e1941b.woff2 +0 -0
  62. package/public/fonts/EarlySummer-VF-Split/c1b593dda62fdeb7dde3af02016da282.woff2 +0 -0
  63. package/public/fonts/EarlySummer-VF-Split/c89f0335910a68a0958f2846108370e8.woff2 +0 -0
  64. package/public/fonts/EarlySummer-VF-Split/ca49aa409fdedd3f2f894cd20a16640a.woff2 +0 -0
  65. package/public/fonts/EarlySummer-VF-Split/ccd4a28d2f63797e0183c87792e20b75.woff2 +0 -0
  66. package/public/fonts/EarlySummer-VF-Split/d2718da923fce8e7ea229d65e306e92c.woff2 +0 -0
  67. package/public/fonts/EarlySummer-VF-Split/d893e9b307d96041e9cfcbd03761b9f4.woff2 +0 -0
  68. package/public/fonts/EarlySummer-VF-Split/dafaedaee41b75e21479d4ff324b6a34.woff2 +0 -0
  69. package/public/fonts/EarlySummer-VF-Split/db392af65f1867e5fd580eed2195df99.woff2 +0 -0
  70. package/public/fonts/EarlySummer-VF-Split/dc7c73a9e5577143ccd11e05ab55cb39.woff2 +0 -0
  71. package/public/fonts/EarlySummer-VF-Split/de396881189f747eba67685298363242.woff2 +0 -0
  72. package/public/fonts/EarlySummer-VF-Split/df625b213228bba22a7733d4eff8f148.woff2 +0 -0
  73. package/public/fonts/EarlySummer-VF-Split/e6e60b384f220b893ef31a926ece829a.woff2 +0 -0
  74. package/public/fonts/EarlySummer-VF-Split/e6e8ce2c5972ab665630bb705383d0fb.woff2 +0 -0
  75. package/public/fonts/EarlySummer-VF-Split/e963c7ed7104c2d6d68fcb5f952fe2f5.woff2 +0 -0
  76. package/public/fonts/EarlySummer-VF-Split/e966b23b4cd7783f43e31032d41784f4.woff2 +0 -0
  77. package/public/fonts/EarlySummer-VF-Split/edaac57c3856ec13128f4c6c3e00975c.woff2 +0 -0
  78. package/public/fonts/EarlySummer-VF-Split/ee54e0d86edf068c6c9cbddb76a856fe.woff2 +0 -0
  79. package/public/fonts/EarlySummer-VF-Split/f612c78a5544ff2dd3e8296ac3e58344.woff2 +0 -0
  80. package/public/fonts/EarlySummer-VF-Split/f9e539bd9b7bf999c3da82f5403ec3b6.woff2 +0 -0
  81. package/public/fonts/EarlySummer-VF-Split/fa5863b923ac15993c52a619f699ee63.woff2 +0 -0
  82. package/public/fonts/EarlySummer-VF-Split/fc759e56ec6f6e6d3d4cb163d62fb557.woff2 +0 -0
  83. package/public/fonts/Font Subset List/CJK Common Characters.txt +7534 -0
  84. package/public/fonts/Font Subset List/EarlySummer Subset.txt +3 -0
  85. package/public/fonts/Font Subset List/Japanese Kana + Korean Letters.txt +6123 -0
  86. package/public/fonts/Font Subset List/Latin + Cyrillic + Greek + Arabic Glyphs.txt +121 -0
  87. package/public/fonts/Font Subset List/unicode_range.py +49 -0
  88. package/public/fonts/NotoSansSC-Bold.otf +0 -0
  89. package/public/fonts/NotoSansSC-Regular.otf +0 -0
  90. package/public/fonts/STIX-Italic-VF.woff2 +0 -0
  91. package/public/fonts/STIX-VF.woff2 +0 -0
  92. package/public/fonts/Snell-Black-SF.woff2 +0 -0
  93. package/public/fonts/Snell-Bold-SF.woff2 +0 -0
  94. package/public/giscus/theme-dark.css +208 -0
  95. package/public/giscus/theme-light.css +208 -0
  96. package/public/icons/favicon.svg +4 -0
  97. package/public/icons/og-logo.png +0 -0
  98. package/public/robots.txt +4 -0
  99. package/public/sounds/tap_01.wav +0 -0
  100. package/public/sounds/tap_02.wav +0 -0
  101. package/public/sounds/tap_03.wav +0 -0
  102. package/public/sounds/tap_04.wav +0 -0
  103. package/public/sounds/tap_05.wav +0 -0
  104. package/public/sounds/type_01.wav +0 -0
  105. package/public/sounds/type_02.wav +0 -0
  106. package/public/sounds/type_03.wav +0 -0
  107. package/public/sounds/type_04.wav +0 -0
  108. package/public/sounds/type_05.wav +0 -0
  109. package/scripts/apply-lqip.ts +276 -0
  110. package/scripts/format-posts.ts +105 -0
  111. package/scripts/migration/README.md +52 -0
  112. package/scripts/migration/migrate-hexo.ts +185 -0
  113. package/scripts/migration/validate-abbrlinks.ts +161 -0
  114. package/scripts/new-post.ts +52 -0
  115. package/scripts/seo/generate-legacy-redirects.ts +407 -0
  116. package/scripts/update-theme.ts +46 -0
  117. package/src/assets/icons/copy-check.svg +3 -0
  118. package/src/assets/icons/copy-icon.svg +4 -0
  119. package/src/assets/icons/go-back.svg +3 -0
  120. package/src/assets/icons/heading-anchor.svg +4 -0
  121. package/src/assets/icons/lang-en.svg +3 -0
  122. package/src/assets/icons/lang-ja.svg +5 -0
  123. package/src/assets/icons/lang-zh.svg +5 -0
  124. package/src/assets/icons/language-switcher.svg +3 -0
  125. package/src/assets/icons/pin-icon.svg +3 -0
  126. package/src/assets/icons/search-icon.svg +3 -0
  127. package/src/assets/icons/theme-toggle.svg +3 -0
  128. package/src/assets/icons/toc-icon.svg +3 -0
  129. package/src/assets/icons/top-icon.svg +3 -0
  130. package/src/assets/lqip-map.json +10 -0
  131. package/src/components/Button.astro +152 -0
  132. package/src/components/CategoryList.astro +66 -0
  133. package/src/components/Comment/Giscus.astro +119 -0
  134. package/src/components/Comment/Index.astro +30 -0
  135. package/src/components/Comment/Twikoo.astro +114 -0
  136. package/src/components/Comment/Waline.astro +149 -0
  137. package/src/components/FloatingButtons.astro +101 -0
  138. package/src/components/Footer.astro +74 -0
  139. package/src/components/Header.astro +62 -0
  140. package/src/components/JournalList.astro +56 -0
  141. package/src/components/Navbar.astro +69 -0
  142. package/src/components/NoteList.astro +56 -0
  143. package/src/components/Pagination.astro +267 -0
  144. package/src/components/PostDate.astro +80 -0
  145. package/src/components/PostList.astro +87 -0
  146. package/src/components/SearchModal.astro +340 -0
  147. package/src/components/TagList.astro +135 -0
  148. package/src/components/Widgets/BackButton.astro +43 -0
  149. package/src/components/Widgets/CodeCopyButton.astro +47 -0
  150. package/src/components/Widgets/GithubCard.astro +110 -0
  151. package/src/components/Widgets/ImageZoom.astro +135 -0
  152. package/src/components/Widgets/MediaEmbed.astro +127 -0
  153. package/src/components/Widgets/SoundEffect.astro +179 -0
  154. package/src/components/Widgets/TOC.astro +198 -0
  155. package/src/config-schema.ts +164 -0
  156. package/src/config.ts +127 -0
  157. package/src/config.ts.example +205 -0
  158. package/src/content/about/_example-about-en.md +6 -0
  159. package/src/content/about/about-en.md +21 -0
  160. package/src/content/about/about-ja.md +21 -0
  161. package/src/content/about/about-zh.md +24 -0
  162. package/src/content.config.ts +247 -0
  163. package/src/env.d.ts +25 -0
  164. package/src/i18n/config.ts +65 -0
  165. package/src/i18n/lang.ts +70 -0
  166. package/src/i18n/path.ts +160 -0
  167. package/src/i18n/ui.ts +214 -0
  168. package/src/layouts/Head.astro +203 -0
  169. package/src/layouts/Layout.astro +69 -0
  170. package/src/pages/404.astro +20 -0
  171. package/src/pages/[...lang]/[...page].astro +48 -0
  172. package/src/pages/[...lang]/about.astro +28 -0
  173. package/src/pages/[...lang]/atom.xml.ts +14 -0
  174. package/src/pages/[...lang]/categories/index.astro +35 -0
  175. package/src/pages/[...lang]/journals/[slug].astro +89 -0
  176. package/src/pages/[...lang]/journals/index.astro +55 -0
  177. package/src/pages/[...lang]/journals/page/[page].astro +66 -0
  178. package/src/pages/[...lang]/notes/[slug].astro +88 -0
  179. package/src/pages/[...lang]/notes/index.astro +55 -0
  180. package/src/pages/[...lang]/notes/page/[page].astro +66 -0
  181. package/src/pages/[...lang]/posts/[slug].astro +101 -0
  182. package/src/pages/[...lang]/rss.xml.ts +14 -0
  183. package/src/pages/[...lang]/search.astro +65 -0
  184. package/src/pages/[...lang]/tags/[tag].astro +53 -0
  185. package/src/pages/[...lang]/tags/index.astro +36 -0
  186. package/src/pages/_dynamic/list.astro +101 -0
  187. package/src/pages/_dynamic/slug.astro +100 -0
  188. package/src/pages/og/[...image].ts +114 -0
  189. package/src/pages/robots.txt.ts +20 -0
  190. package/src/plugins/rehype-code-copy-button.mjs +82 -0
  191. package/src/plugins/rehype-external-links.mjs +18 -0
  192. package/src/plugins/rehype-heading-anchor.mjs +55 -0
  193. package/src/plugins/rehype-image-processor.mjs +77 -0
  194. package/src/plugins/remark-container-directives.mjs +135 -0
  195. package/src/plugins/remark-leaf-directives.mjs +184 -0
  196. package/src/plugins/remark-reading-time.mjs +11 -0
  197. package/src/styles/comment.css +205 -0
  198. package/src/styles/extension.css +180 -0
  199. package/src/styles/font.css +111 -0
  200. package/src/styles/global.css +91 -0
  201. package/src/styles/lqip.css +71 -0
  202. package/src/styles/markdown.css +276 -0
  203. package/src/styles/transition.css +173 -0
  204. package/src/types/global.d.ts +22 -0
  205. package/src/types/index.d.ts +111 -0
  206. package/src/utils/cache.ts +32 -0
  207. package/src/utils/content.ts +819 -0
  208. package/src/utils/description.ts +147 -0
  209. package/src/utils/dynamic-collections.ts +155 -0
  210. package/src/utils/feed.ts +238 -0
  211. package/src/utils/page.ts +107 -0
  212. package/tsconfig.json +13 -0
  213. package/uno.config.ts +75 -0
@@ -0,0 +1,70 @@
1
+ import type { Language } from '@/i18n/config'
2
+ import { allLocales, base, defaultLocale, moreLocales } from '@/config'
3
+ import { langMap } from '@/i18n/config'
4
+
5
+ /**
6
+ * Get the short language code for the `[...lang]` route parameter
7
+ *
8
+ * @param lang Current language code (e.g. 'en')
9
+ * @returns Route parameter value (e.g. 'en') or undefined (root path '/')
10
+ */
11
+ export function getLangRouteParam(lang: Language): string | undefined {
12
+ return lang === defaultLocale ? undefined : lang
13
+ }
14
+
15
+ /**
16
+ * Get the corresponding short language code from the complete current locale value
17
+ *
18
+ * @param locale Current locale value (e.g. 'en-US')
19
+ * @returns Corresponding language code (e.g. 'en') or default locale
20
+ */
21
+ export function getLangFromLocale(locale: string | undefined): Language {
22
+ if (!locale) {
23
+ return defaultLocale
24
+ }
25
+
26
+ const match = Object.entries(langMap).find(([, codes]) =>
27
+ (codes as readonly string[]).includes(locale),
28
+ )
29
+ return (match?.[0] ?? defaultLocale) as Language
30
+ }
31
+
32
+ /**
33
+ * Get the language code from the current path
34
+ *
35
+ * @param path Current page path
36
+ * @returns Language code detected from path or default locale
37
+ */
38
+ export function getLangFromPath(path: string): Language {
39
+ const pathWithoutBase = base && path.startsWith(base)
40
+ ? path.slice(base.length)
41
+ : path
42
+
43
+ const normalized = pathWithoutBase.replace(/^\/+/, '')
44
+ if (!normalized) {
45
+ return defaultLocale
46
+ }
47
+
48
+ const [firstSegment] = normalized.split('/')
49
+ const maybeLang = firstSegment.replace(/\.html$/, '') as Language
50
+
51
+ return moreLocales.includes(maybeLang) ? maybeLang : defaultLocale
52
+ }
53
+
54
+ /**
55
+ * Get the next language code in the global language cycle
56
+ *
57
+ * @param currentLang Current language code
58
+ * @returns Next language code in the global cycle
59
+ */
60
+ export function getNextGlobalLang(currentLang: Language): Language {
61
+ // Get index of current language
62
+ const currentIndex = allLocales.indexOf(currentLang)
63
+ if (currentIndex === -1) {
64
+ return defaultLocale
65
+ }
66
+
67
+ // Calculate and return next language in cycle
68
+ const nextIndex = (currentIndex + 1) % allLocales.length
69
+ return allLocales[nextIndex]
70
+ }
@@ -0,0 +1,160 @@
1
+ import type { Language } from '@/i18n/config'
2
+ import { allLocales, base, defaultLocale } from '@/config'
3
+ import { getLangFromPath, getNextGlobalLang } from '@/i18n/lang'
4
+
5
+ function stripLeadingLang(path: string, lang: Language): string {
6
+ const normalized = path.replace(/^\/+/, '')
7
+ if (!normalized) {
8
+ return '/'
9
+ }
10
+
11
+ const [first, ...rest] = normalized.split('/')
12
+ const firstNoHtml = first.replace(/\.html$/, '')
13
+ if (firstNoHtml !== lang) {
14
+ return path
15
+ }
16
+
17
+ // /en , /en.html -> /
18
+ if (rest.length === 0) {
19
+ return '/'
20
+ }
21
+
22
+ const restPath = `/${rest.join('/')}`
23
+ // Guard against bad states like /en/en.html
24
+ if (restPath === `/${lang}` || restPath === `/${lang}.html`) {
25
+ return '/'
26
+ }
27
+
28
+ return restPath
29
+ }
30
+
31
+ /**
32
+ * Get path to a specific tag page with language support
33
+ */
34
+ export function getTagPath(tagName: string, lang: Language): string {
35
+ const encodedTag = encodeURIComponent(tagName)
36
+ const tagPath = lang === defaultLocale
37
+ ? `/tags/${encodedTag}`
38
+ : `/${lang}/tags/${encodedTag}`
39
+
40
+ return base ? `${base}${tagPath}` : tagPath
41
+ }
42
+
43
+ /**
44
+ * Get path to a specific post page with language and category support
45
+ */
46
+ export function getPostPath(slug: string, lang: Language): string {
47
+ const postPath = lang === defaultLocale
48
+ ? `/posts/${slug}`
49
+ : `/${lang}/posts/${slug}`
50
+
51
+ return base ? `${base}${postPath}` : postPath
52
+ }
53
+
54
+ /**
55
+ * Get path to a specific note page with language support
56
+ */
57
+ export function getNotePath(slug: string, lang: Language): string {
58
+ const notePath = lang === defaultLocale
59
+ ? `/notes/${slug}`
60
+ : `/${lang}/notes/${slug}`
61
+
62
+ return base ? `${base}${notePath}` : notePath
63
+ }
64
+
65
+ /**
66
+ * Get path to a specific journal page with language support
67
+ */
68
+ export function getJournalPath(slug: string, lang: Language): string {
69
+ const journalPath = lang === defaultLocale
70
+ ? `/journals/${slug}`
71
+ : `/${lang}/journals/${slug}`
72
+
73
+ return base ? `${base}${journalPath}` : journalPath
74
+ }
75
+
76
+ /**
77
+ * Generate localized path based on current language
78
+ *
79
+ * @param path Path to localize
80
+ * @param currentLang Current language code
81
+ * @returns Localized path with language prefix
82
+ */
83
+ export function getLocalizedPath(path: string, currentLang?: Language) {
84
+ // Astro 6 with build.format: 'file' emits dist/index.html and reports
85
+ // `Astro.url.pathname` as `/index.html` for the homepage, so we also
86
+ // need to recognise the stripped `index` form as "the homepage".
87
+ const normalizedPath = path
88
+ .replace(/^\/|\/$/g, '')
89
+ .replace(/\.html$/, '')
90
+ const isHomepage = normalizedPath === '' || normalizedPath === 'index'
91
+ const lang = currentLang ?? getLangFromPath(path)
92
+
93
+ const langPrefix = lang === defaultLocale ? '' : `/${lang}`
94
+ // Don't add trailing slash since trailingSlash is set to 'never'
95
+ const localizedPath = isHomepage
96
+ ? `${langPrefix || '/'}`
97
+ : `${langPrefix}/${normalizedPath}`
98
+
99
+ return base ? `${base}${localizedPath}` : localizedPath
100
+ }
101
+
102
+ /**
103
+ * Build path for next language
104
+ *
105
+ * @param currentPath Current page path
106
+ * @param currentLang Current language code
107
+ * @param nextLang Next language code to switch to
108
+ * @returns Path for next language
109
+ */
110
+ export function getNextLangPath(currentPath: string, currentLang: Language, nextLang: Language): string {
111
+ const pathWithoutBase = base && currentPath.startsWith(base)
112
+ ? currentPath.slice(base.length)
113
+ : currentPath
114
+
115
+ const pagePath = currentLang === defaultLocale
116
+ ? pathWithoutBase
117
+ : stripLeadingLang(pathWithoutBase, currentLang)
118
+
119
+ return getLocalizedPath(pagePath, nextLang)
120
+ }
121
+
122
+ /**
123
+ * Get next language path from global language list
124
+ *
125
+ * @param currentPath Current page path
126
+ * @returns Path for next supported language
127
+ */
128
+ export function getNextGlobalLangPath(currentPath: string): string {
129
+ const currentLang = getLangFromPath(currentPath)
130
+ const nextLang = getNextGlobalLang(currentLang)
131
+ return getNextLangPath(currentPath, currentLang, nextLang)
132
+ }
133
+
134
+ /**
135
+ * Get next language path from supported language list
136
+ *
137
+ * @param currentPath Current page path
138
+ * @param supportedLangs List of supported language codes
139
+ * @returns Path for next supported language
140
+ */
141
+ export function getNextSupportedLangPath(currentPath: string, supportedLangs: Language[]): string {
142
+ if (supportedLangs.length === 0) {
143
+ return getNextGlobalLangPath(currentPath)
144
+ }
145
+
146
+ // Sort supported languages by global priority
147
+ const langPriority = new Map<Language, number>(
148
+ allLocales.map((lang, index) => [lang, index]),
149
+ )
150
+ const sortedLangs = [...supportedLangs].sort(
151
+ (a, b) => (langPriority.get(a) ?? 0) - (langPriority.get(b) ?? 0),
152
+ )
153
+
154
+ // Get current language and next in cycle
155
+ const currentLang = getLangFromPath(currentPath)
156
+ const currentIndex = sortedLangs.indexOf(currentLang)
157
+ const nextLang = sortedLangs[(currentIndex + 1) % sortedLangs.length]
158
+
159
+ return getNextLangPath(currentPath, currentLang, nextLang)
160
+ }
package/src/i18n/ui.ts ADDED
@@ -0,0 +1,214 @@
1
+ import type { Language } from '@/i18n/config'
2
+
3
+ interface Translation {
4
+ title: string
5
+ subtitle: string
6
+ description: string
7
+ posts: string
8
+ notes?: string
9
+ notesIntro?: string
10
+ notesEmpty?: string
11
+ journals?: string
12
+ journalsIntro?: string
13
+ journalsEmpty?: string
14
+ tags: string
15
+ about: string
16
+ search: string
17
+ searchPlaceholder: string
18
+ searchNoResults: string
19
+ searchResultsFound: string
20
+ toc: string
21
+ tech?: string
22
+ life?: string
23
+ science?: string
24
+ categories?: string
25
+ categoriesInTotal?: string
26
+ tagsInTotal?: string
27
+ }
28
+
29
+ export const ui: Record<Language, Translation> = {
30
+ 'de': {
31
+ title: 'Neusatz',
32
+ subtitle: 'Die Schönheit der Typografie wiederbeleben',
33
+ description: 'Retypeset ist ein statisches Blog-Theme basierend auf dem Astro-Framework, auf Deutsch bekannt als "Neusatz". Dieses Theme, inspiriert von traditioneller Typografie, etabliert einen neuen visuellen Standard und gestaltet alle Seiten neu, um ein Leseerlebnis ähnlich dem gedruckter Bücher zu schaffen und die Schönheit des Satzes wiederzubeleben. Jedes Element ist bis ins kleinste Detail durchdacht, Eleganz zeigt sich auch im kleinsten Raum.',
34
+ posts: 'Beiträge',
35
+ tags: 'Schlagwörter',
36
+ about: 'Über',
37
+ search: 'Suche',
38
+ searchPlaceholder: 'Suchbegriff eingeben...',
39
+ searchNoResults: 'Keine Ergebnisse gefunden',
40
+ searchResultsFound: 'Ergebnisse gefunden',
41
+ toc: 'Inhaltsverzeichnis',
42
+ },
43
+ 'en': {
44
+ title: 'Life Odyssey',
45
+ subtitle: 'A journey through life, technology, and reflections',
46
+ description: 'A personal blog about life, technology, and reflections.',
47
+ posts: 'Posts',
48
+ notes: 'Notes',
49
+ notesIntro: 'Personal study notes (often rough, incomplete, and subject to change).',
50
+ notesEmpty: 'No notes yet.',
51
+ journals: 'Journal',
52
+ journalsIntro: 'Personal journal entries (diary-like and unpolished).',
53
+ journalsEmpty: 'No journal entries yet.',
54
+ tags: 'Tags',
55
+ about: 'About',
56
+ search: 'Search',
57
+ searchPlaceholder: 'Type to search...',
58
+ searchNoResults: 'No results found',
59
+ searchResultsFound: 'results found',
60
+ toc: 'Table of Contents',
61
+ tech: 'Tech',
62
+ life: 'Life',
63
+ science: 'Science',
64
+ categories: 'Categories',
65
+ categoriesInTotal: 'categories in total',
66
+ tagsInTotal: 'tags in total',
67
+ },
68
+ 'es': {
69
+ title: 'Retipografía',
70
+ subtitle: 'Reviviendo la belleza tipográfica',
71
+ description: 'Retypeset es un tema de blog estático basado en el framework Astro. Inspirado por Typography, Retypeset establece un nuevo estándar visual y reimagina el diseño de todas las páginas, creando una experiencia de lectura similar a la de los libros impresos, reviviendo la belleza de la tipografía. Detalles en cada mirada, elegancia en cada espacio.',
72
+ posts: 'Artículos',
73
+ tags: 'Etiquetas',
74
+ about: 'Sobre',
75
+ search: 'Buscar',
76
+ searchPlaceholder: 'Escribe para buscar...',
77
+ searchNoResults: 'No se encontraron resultados',
78
+ searchResultsFound: 'resultados encontrados',
79
+ toc: 'Índice',
80
+ },
81
+ 'fr': {
82
+ title: 'Retypographie',
83
+ subtitle: 'Raviver la beauté de la typographie',
84
+ description: 'Retypeset est un thème de blog statique basé sur le framework Astro, connu en français sous le nom de "Retypographie". Ce thème, inspiré par la typographie traditionnelle, établit une nouvelle norme visuelle et réorganise toutes les pages pour créer une expérience de lecture semblable à celle des livres imprimés, ravivant ainsi la beauté de la mise en page. Chaque élément est soigné dans les moindres détails, l\'élégance se manifeste dans les plus petits espaces.',
85
+ posts: 'Articles',
86
+ tags: 'Étiquettes',
87
+ about: 'À propos',
88
+ search: 'Recherche',
89
+ searchPlaceholder: 'Tapez pour rechercher...',
90
+ searchNoResults: 'Aucun résultat trouvé',
91
+ searchResultsFound: 'résultats trouvés',
92
+ toc: 'Table des matières',
93
+ },
94
+ 'ja': {
95
+ title: 'Life Odyssey',
96
+ subtitle: '人生、技術、そして思索の旅',
97
+ description: '人生、技術、思索についての個人ブログ。',
98
+ posts: '記事',
99
+ notes: 'ノート',
100
+ notesIntro: '学習・問題演習中の個人メモです。未整理・未完成の内容が含まれ、予告なく更新されます。',
101
+ notesEmpty: 'ノートはまだありません。',
102
+ journals: '日記',
103
+ journalsIntro: '日々の記録です(私的な内容を含み、未整理なことがあります)。',
104
+ journalsEmpty: '日記はまだありません。',
105
+ tags: 'タグ',
106
+ about: '概要',
107
+ search: '検索',
108
+ searchPlaceholder: '検索キーワードを入力...',
109
+ searchNoResults: '結果が見つかりません',
110
+ searchResultsFound: '件の結果',
111
+ toc: '目次',
112
+ tech: '技術',
113
+ life: '生活',
114
+ science: '科学',
115
+ categories: 'カテゴリー',
116
+ tagsInTotal: '個のタグ',
117
+ },
118
+ 'ko': {
119
+ title: '재조판',
120
+ subtitle: '판형의 아름다움을 재현하다',
121
+ description: 'Retypeset은 Astro 프레임워크를 기반으로 한 정적 블로그 테마로, 한국어로는 "재조판"이라고 합니다. 이 테마는 활판 인쇄에서 디자인 영감을 얻어, 새로운 시각적 기준을 확립하고 모든 페이지를 재구성하여 종이책과 같은 독서 경험을 제공하며 판형의 아름다움을 되살립니다. 모든 것이 세부적인 디테일이며, 작은 공간에서도 우아함이 느껴집니다.',
122
+ posts: '게시물',
123
+ tags: '태그',
124
+ about: '소개',
125
+ search: '검색',
126
+ searchPlaceholder: '검색어를 입력하세요...',
127
+ searchNoResults: '결과를 찾을 수 없습니다',
128
+ searchResultsFound: '개의 결과',
129
+ toc: '목차',
130
+ },
131
+ 'pl': {
132
+ title: 'Przeskład',
133
+ subtitle: 'Ożywiając piękno typografii',
134
+ description: 'Retypeset to statyczny motyw bloga oparty na frameworku Astro, w języku polskim znany jako "Przeskład". Ten motyw, inspirowany typografią drukarską, ustanawia nowy standard wizualny i reorganizuje wszystkie strony, tworząc doświadczenie czytelnicze przypominające papierowe książki, przywracając piękno układu tekstu. Każdy element jest dopracowany w szczegółach, elegancja zawarta w najmniejszej przestrzeni.',
135
+ posts: 'Artykuły',
136
+ tags: 'Tagi',
137
+ about: 'O stronie',
138
+ search: 'Szukaj',
139
+ searchPlaceholder: 'Wpisz, aby wyszukać...',
140
+ searchNoResults: 'Nie znaleziono wyników',
141
+ searchResultsFound: 'wyników',
142
+ toc: 'Spis treści',
143
+ },
144
+ 'pt': {
145
+ title: 'Retipografia',
146
+ subtitle: 'Reviva a beleza da tipografia',
147
+ description: 'Retypeset é um tema de blog estático baseado no framework Astro. Inspirado pela tipografia, o Retypeset estabelece um novo padrão visual e reimagina o layout de todas as páginas, criando uma experiência de leitura reminiscente de livros físicos, revivendo a beleza da tipografia. Cada detalhe é visível, elegância em cada espaço.',
148
+ posts: 'Artigos',
149
+ tags: 'Tags',
150
+ about: 'Sobre',
151
+ search: 'Pesquisar',
152
+ searchPlaceholder: 'Digite para pesquisar...',
153
+ searchNoResults: 'Nenhum resultado encontrado',
154
+ searchResultsFound: 'resultados encontrados',
155
+ toc: 'Sumário',
156
+ },
157
+ 'ru': {
158
+ title: 'Переверстка',
159
+ subtitle: 'Возрождая красоту типографики',
160
+ description: 'Retypeset — это статическая тема блога, основанная на фреймворке Astro. Вдохновленная Typography, Retypeset устанавливает новый визуальный стандарт и переосмысливает компоновку всех страниц, создавая опыт чтения, напоминающий печатные книги, возрождая красоту типографики. Детали в каждом взгляде, элегантность в каждом пространстве.',
161
+ posts: 'Посты',
162
+ tags: 'Теги',
163
+ about: 'О себе',
164
+ search: 'Поиск',
165
+ searchPlaceholder: 'Введите для поиска...',
166
+ searchNoResults: 'Результаты не найдены',
167
+ searchResultsFound: 'результатов',
168
+ toc: 'Оглавление',
169
+ },
170
+ 'zh': {
171
+ title: 'Life Odyssey',
172
+ subtitle: '人生若只如初见',
173
+ description: '关于生活、技术与思考的个人博客。',
174
+ posts: '文章',
175
+ notes: '笔记',
176
+ notesIntro: '这里主要是我学习、刷题时的随手记录,偏自用,可能不完整或随时修改。',
177
+ notesEmpty: '还没有笔记。',
178
+ journals: '日记',
179
+ journalsIntro: 'this is my odyssey',
180
+ journalsEmpty: '还没有日记。',
181
+ tags: '标签',
182
+ about: '关于',
183
+ search: '搜索',
184
+ searchPlaceholder: '输入关键词搜索...',
185
+ searchNoResults: '没有找到结果',
186
+ searchResultsFound: '个结果',
187
+ toc: '目录',
188
+ tech: '技术',
189
+ life: '生活',
190
+ science: '科研',
191
+ categories: '分类',
192
+ categoriesInTotal: '个分类',
193
+ tagsInTotal: '个标签',
194
+ },
195
+ 'zh-tw': {
196
+ title: '重新編排',
197
+ subtitle: '再現版式之美',
198
+ description: 'Retypeset是一款基於Astro框架的靜態部落格主題,中文名為重新編排。本主題以活版印字為設計靈感,通過建立全新的視覺規範,對所有頁面進行重新編排,打造紙質書頁般的閱讀體驗,再現版式之美。所見皆為細節,方寸盡顯優雅。',
199
+ posts: '文章',
200
+ tags: '標籤',
201
+ about: '關於',
202
+ search: '搜尋',
203
+ searchPlaceholder: '輸入關鍵字搜尋...',
204
+ searchNoResults: '沒有找到結果',
205
+ searchResultsFound: '個結果',
206
+ toc: '目錄',
207
+ tech: '技術',
208
+ life: '生活',
209
+ science: '科研',
210
+ categories: '分類',
211
+ categoriesInTotal: '個分類',
212
+ tagsInTotal: '個標籤',
213
+ },
214
+ }
@@ -0,0 +1,203 @@
1
+ ---
2
+ import { ClientRouter } from 'astro:transitions'
3
+ import katexCSS from 'katex/dist/katex.min.css?url'
4
+ import { allLocales, base, defaultLocale, themeConfig } from '@/config'
5
+ import { ui } from '@/i18n/ui'
6
+ import { getPageInfo } from '@/utils/page'
7
+
8
+ interface Props {
9
+ postTitle?: string
10
+ postDescription?: string
11
+ postSlug?: string
12
+ }
13
+
14
+ // Props and Language
15
+ const { postTitle, postDescription, postSlug } = Astro.props
16
+ const { currentLang } = getPageInfo(Astro.url.pathname)
17
+ const currentUI = ui[currentLang as keyof typeof ui] ?? {}
18
+ const lang = currentLang === defaultLocale ? '' : `${currentLang}/`
19
+
20
+ // Site Configuration
21
+ const { title, subtitle, description, i18nTitle, author, favicon } = themeConfig.site
22
+ const { mode: defaultMode, light: { background: lightMode }, dark: { background: darkMode } } = themeConfig.color
23
+ const { katex: katexEnabled, reduceMotion } = themeConfig.global
24
+ const { verification = {}, twitterID = '', googleAnalyticsID = '', umamiAnalyticsID = '', apiflashKey = '' } = themeConfig.seo ?? {}
25
+ const { google = '', bing = '', yandex = '', baidu = '' } = verification
26
+ const { customGoogleAnalyticsJS = '', customUmamiAnalyticsJS = '' } = themeConfig.preload ?? {}
27
+
28
+ // Site Metadata
29
+ const metaTheme = defaultMode === 'dark' ? darkMode : lightMode
30
+ const siteTitle = i18nTitle ? currentUI.title : title
31
+ const siteSubtitle = i18nTitle ? currentUI.subtitle : subtitle
32
+ const siteDescription = i18nTitle ? currentUI.description : description
33
+
34
+ // Page Metadata
35
+ const pageTitle = postTitle ? `${postTitle} | ${siteTitle}` : `${siteTitle} - ${siteSubtitle}`
36
+ const pageDescription = postDescription || siteDescription
37
+ const pageImage = postSlug
38
+ ? new URL(`${base}/og/${postSlug}.png`, Astro.url.origin)
39
+ : apiflashKey
40
+ ? `https://api.apiflash.com/v1/urltoimage?access_key=${apiflashKey}&url=${Astro.url}&format=png&width=1500&height=788&ttl=259200&wait_until=network_idle&no_tracking=true`
41
+ : `https://api.apiflash.com/v1/urltoimage?access_key=02a837b6188f4ba0a7fd9fbeff03a83e&url=https://retypeset.radishzz.cc/${lang}&format=png&width=1500&height=788&ttl=604800&wait_until=network_idle&no_tracking=true`
42
+ ---
43
+
44
+ <head>
45
+ <!-- Basic Info -->
46
+ <meta charset="utf-8" />
47
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
48
+ {favicon.toLowerCase().endsWith('.svg') && <link rel="icon" type="image/svg+xml" href={`${base}${favicon}`} />}
49
+ {favicon.toLowerCase().endsWith('.png') && <link rel="icon" type="image/png" href={`${base}${favicon}`} />}
50
+ {favicon.toLowerCase().endsWith('.ico') && <link rel="icon" type="image/x-icon" href={`${base}${favicon}`} />}
51
+ <title>{pageTitle}</title>
52
+ <meta name="description" content={pageDescription} />
53
+ <meta name="author" content={author} />
54
+ <meta name="generator" content={Astro.generator} />
55
+ <meta name="theme-color" content={metaTheme} />
56
+
57
+ <!-- Preload -->
58
+ <link rel="preload" href={`${base}/fonts/EarlySummer-VF-Split/EarlySummer-VF-Subset.woff2`} as="font" type="font/woff2" crossorigin />
59
+ <link rel="preload" href={`${base}/fonts/Snell-Black-SF.woff2`} as="font" type="font/woff2" crossorigin />
60
+ <link rel="preload" href={`${base}/fonts/Snell-Bold-SF.woff2`} as="font" type="font/woff2" crossorigin />
61
+ <link rel="preload" href={`${base}/fonts/STIX-VF.woff2`} as="font" type="font/woff2" crossorigin />
62
+ <link rel="preload" href={`${base}/fonts/STIX-Italic-VF.woff2`} as="font" type="font/woff2" crossorigin />
63
+ {katexEnabled && <link rel="stylesheet" href={katexCSS} media="print" onload={`this.media='all'`} />}
64
+
65
+ <!-- Site Links -->
66
+ <link rel="canonical" href={Astro.url} />
67
+ <link rel="sitemap" href={`${base}/sitemap-index.xml`} />
68
+ <link rel="alternate" href={`${base}/rss.xml`} type="application/rss+xml" title="RSS Feed" />
69
+ <link rel="alternate" href={`${base}/atom.xml`} type="application/atom+xml" title="Atom Feed" />
70
+ {allLocales.map(lang => (
71
+ <link
72
+ rel="alternate"
73
+ href={`${Astro.url.origin}${base}${lang === defaultLocale ? '' : `/${lang}/`}`}
74
+ hreflang={lang === 'zh-tw' ? 'zh-TW' : lang}
75
+ />
76
+ ))}
77
+
78
+ <!-- Open Graph -->
79
+ <meta property="og:type" content={postTitle ? 'article' : 'website'} />
80
+ <meta property="og:url" content={Astro.url} />
81
+ <meta property="og:title" content={pageTitle} />
82
+ <meta property="og:description" content={pageDescription} />
83
+ <meta property="og:image" content={pageImage} />
84
+ <meta name="twitter:card" content="summary_large_image" />
85
+ {twitterID && <meta name="twitter:site" content={twitterID} />}
86
+
87
+ <!-- Site Verification -->
88
+ {google && <meta name="google-site-verification" content={google} />}
89
+ {bing && <meta name="msvalidate.01" content={bing} />}
90
+ {yandex && <meta name="yandex-verification" content={yandex} />}
91
+ {baidu && <meta name="baidu-site-verification" content={baidu} />}
92
+
93
+ <!-- Global View Transition -->
94
+ <ClientRouter fallback="none" />
95
+
96
+ <!-- Theme Toggle -->
97
+ <script
98
+ is:inline
99
+ define:vars={{
100
+ defaultMode,
101
+ lightMode,
102
+ darkMode,
103
+ reduceMotion,
104
+ }}
105
+ >
106
+ (function () {
107
+ // Check if current theme is dark
108
+ // Priority: localStorage theme > default theme > system preference
109
+ function isCurrentDark() {
110
+ const currentTheme = localStorage.getItem('theme')
111
+ if (currentTheme) {
112
+ return currentTheme === 'dark'
113
+ }
114
+
115
+ if (defaultMode !== 'auto') {
116
+ return defaultMode === 'dark'
117
+ }
118
+
119
+ // If defaultMode is auto, follow system preference
120
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
121
+ }
122
+
123
+ // Initialize theme
124
+ function initTheme(doc = document) {
125
+ const isDark = isCurrentDark()
126
+ doc.documentElement.classList.toggle('dark', isDark)
127
+
128
+ // Update meta theme-color tag
129
+ const metaThemeColor = doc.head.querySelector('meta[name="theme-color"]')
130
+ if (metaThemeColor) {
131
+ metaThemeColor.setAttribute('content', isDark ? darkMode : lightMode)
132
+ }
133
+ }
134
+
135
+ // Check and init motion preference
136
+ // Condition: theme config || system preference || browser capability
137
+ function initMotionPref(doc = document) {
138
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
139
+ const supportsViewTransitions = 'startViewTransition' in doc
140
+ const shouldReduceMotion = reduceMotion || prefersReducedMotion || !supportsViewTransitions
141
+
142
+ doc.documentElement.classList.toggle('reduce-motion', shouldReduceMotion)
143
+ }
144
+
145
+ // Update theme before page transition to prevent flashing
146
+ document.addEventListener('astro:before-swap', ({ newDocument }) => {
147
+ initTheme(newDocument)
148
+ initMotionPref(newDocument)
149
+ })
150
+
151
+ // Initialize theme on first load
152
+ initTheme()
153
+ initMotionPref()
154
+ })()
155
+ </script>
156
+
157
+ <!-- Google Analytics -->
158
+ <!-- Define gtag on window object for proper Partytown forwarding -->
159
+ <!-- See https://github.com/QwikDev/partytown/issues/382 -->
160
+ {googleAnalyticsID && (
161
+ <>
162
+ <script
163
+ is:inline
164
+ type="text/partytown"
165
+ src={customGoogleAnalyticsJS || `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsID}`}
166
+ />
167
+ <script
168
+ is:inline
169
+ type="text/partytown"
170
+ define:vars={{
171
+ googleAnalyticsID,
172
+ customGoogleAnalyticsJS,
173
+ }}
174
+ >
175
+ window.dataLayer = window.dataLayer || []
176
+ window.gtag = function () {
177
+ // eslint-disable-next-line prefer-rest-params
178
+ dataLayer.push(arguments)
179
+ }
180
+ window.gtag('js', new Date())
181
+
182
+ if (customGoogleAnalyticsJS) {
183
+ window.gtag('config', googleAnalyticsID, {
184
+ transport_url: new URL(customGoogleAnalyticsJS).origin,
185
+ })
186
+ }
187
+ else {
188
+ window.gtag('config', googleAnalyticsID)
189
+ }
190
+ </script>
191
+ </>
192
+ )}
193
+
194
+ <!-- Umami Analytics -->
195
+ {umamiAnalyticsID && (
196
+ <script
197
+ is:inline
198
+ type="text/partytown"
199
+ src={customUmamiAnalyticsJS || 'https://cloud.umami.is/script.js'}
200
+ data-website-id={umamiAnalyticsID}
201
+ />
202
+ )}
203
+ </head>