Mirror of
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: bootstrap project

+958 -69
+1 -47
README.md
··· 1 - # Astro Starter Kit: Minimal 2 - 3 - ```sh 4 - pnpm create astro@latest -- --template minimal 5 - ``` 6 - 7 - [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) 8 - [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal) 9 - [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json) 10 - 11 - > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 - 13 - ## 🚀 Project Structure 14 - 15 - Inside of your Astro project, you'll see the following folders and files: 16 - 17 - ```text 18 - / 19 - ├── public/ 20 - ├── src/ 21 - │ └── pages/ 22 - │ └── index.astro 23 - └── package.json 24 - ``` 25 - 26 - Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 27 - 28 - There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 29 - 30 - Any static assets, like images, can be placed in the `public/` directory. 31 - 32 - ## 🧞 Commands 33 - 34 - All commands are run from the root of the project, from a terminal: 35 - 36 - | Command | Action | 37 - | :------------------------ | :----------------------------------------------- | 38 - | `pnpm install` | Installs dependencies | 39 - | `pnpm dev` | Starts local dev server at `localhost:4321` | 40 - | `pnpm build` | Build your production site to `./dist/` | 41 - | `pnpm preview` | Preview your build locally, before deploying | 42 - | `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` | 43 - | `pnpm astro -- --help` | Get help using the Astro CLI | 44 - 45 - ## 👀 Want to learn more? 46 - 47 - Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 1 + # Starlight Plugins Translation Tracker
+2 -2
package.json
··· 1 1 { 2 2 "name": "starlight-plugin-translations", 3 3 "type": "module", 4 - "version": "0.0.1", 4 + "version": "0.1.0", 5 5 "scripts": { 6 6 "dev": "astro dev", 7 7 "build": "astro build", ··· 11 11 "dependencies": { 12 12 "astro": "^5.10.1" 13 13 } 14 - } 14 + }
+28 -8
public/favicon.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"> 2 - <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" /> 3 - <style> 4 - path { fill: #000; } 5 - @media (prefers-color-scheme: dark) { 6 - path { fill: #FFF; } 7 - } 8 - </style> 1 + <svg width="16" height="16" viewBox="0 0 441 441" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <g clip-path="url(#clip0_18_385)"> 3 + <circle cx="220.5" cy="220.5" r="220.5" fill="#C992FB"/> 4 + <path d="M317.98 22.6625C293.647 13.1947 267.18 8 239.5 8C119.93 8 23 104.93 23 224.5C23 340.901 114.861 435.847 230.044 440.797C226.879 440.932 223.698 441 220.5 441C98.7212 441 0 342.279 0 220.5C0 98.7212 98.7212 0 220.5 0C255.496 0 288.588 8.1527 317.98 22.6625Z" fill="#E0BEFF"/> 5 + <path d="M169 407C297.13 407 401 303.13 401 175C401 132.194 389.407 92.0951 369.189 57.6729C413.318 97.993 441 156.014 441 220.5C441 342.278 342.279 441 220.5 441C165.836 441 115.818 421.108 77.2891 388.168C105.419 400.286 136.426 407 169 407Z" fill="#6A2F9E"/> 6 + <path d="M169 407C297.13 407 401 303.13 401 175C401 132.194 389.407 92.0951 369.189 57.6729C413.318 97.993 441 156.014 441 220.5C441 342.278 342.279 441 220.5 441C165.836 441 115.818 421.108 77.2891 388.168C105.419 400.286 136.426 407 169 407Z" fill="#6A2F9E"/> 7 + <path d="M169 407C297.13 407 401 303.13 401 175C401 132.194 389.407 92.0951 369.189 57.6729C413.318 97.993 441 156.014 441 220.5C441 342.278 342.279 441 220.5 441C165.836 441 115.818 421.108 77.2891 388.168C105.419 400.286 136.426 407 169 407Z" fill="#A551F2"/> 8 + <path d="M223.12 278.35C234.71 297.75 262.15 298.5 275.89 281.13C278.03 278.42 280.19 272.93 283.5 271.02C290.99 266.68 297.79 275.25 295.23 282.23C282 318.22 228.47 321.79 209.62 288.91C206.42 283.33 204.82 273.57 213.26 271.8C218.45 270.71 220.47 273.92 223.12 278.35Z" fill="#20103F"/> 9 + <path d="M376 191C376 219.167 353.167 242 325 242C296.833 242 274 219.167 274 191C274 162.833 296.833 140 325 140C353.167 140 376 162.833 376 191Z" fill="#20103F"/> 10 + <path d="M342.36 205.956C352.856 209.859 364.487 204.629 368.338 194.273C372.19 183.918 366.803 172.358 356.307 168.455C345.811 164.551 334.18 169.782 330.329 180.138C326.477 190.493 331.864 202.052 342.36 205.956Z" fill="#FCFCFE"/> 11 + <path d="M202 201C202 229.167 179.167 252 151 252C122.833 252 100 229.167 100 201C100 172.833 122.833 150 151 150C179.167 150 202 172.833 202 201Z" fill="#20103F"/> 12 + <path d="M168.36 215.956C178.856 219.859 190.487 214.629 194.338 204.273C198.19 193.918 192.803 182.358 182.307 178.455C171.811 174.551 160.18 179.782 156.329 190.138C152.477 200.493 157.864 212.052 168.36 215.956Z" fill="#FCFCFE"/> 13 + <circle cx="388" cy="317" r="11" fill="#C992FB"/> 14 + <circle cx="328" cy="352" r="23" fill="#20103F"/> 15 + <circle cx="275.5" cy="393.5" r="7.5" fill="#20103F"/> 16 + <circle cx="184" cy="366" r="12" fill="#20103F"/> 17 + <circle cx="126" cy="306" r="30" fill="#20103F"/> 18 + <circle cx="59" cy="267" r="7" fill="#20103F"/> 19 + <circle cx="71" cy="180" r="7" fill="#20103F"/> 20 + <circle cx="189.5" cy="86.5" r="21.5" fill="#20103F"/> 21 + <circle cx="264" cy="118" r="12" fill="#20103F"/> 22 + <circle cx="264" cy="70" r="5" fill="#20103F"/> 23 + </g> 24 + <defs> 25 + <clipPath id="clip0_18_385"> 26 + <rect width="441" height="441" fill="white"/> 27 + </clipPath> 28 + </defs> 9 29 </svg>
+10
src/components/Link.astro
··· 1 + --- 2 + interface Props { 3 + href: string; 4 + text: string; 5 + } 6 + 7 + const { href, text } = Astro.props as Props; 8 + --- 9 + 10 + <a href={href}>{text}</a>
+88
src/components/LocaleDetails.astro
··· 1 + --- 2 + import type { LocaleKeys, Progress } from '../schemas'; 3 + import Link from './Link.astro'; 4 + import ProgressBar from './ProgressBar.astro'; 5 + 6 + interface Props { 7 + localeKeys: LocaleKeys 8 + } 9 + 10 + const { 11 + localeKeys 12 + } = Astro.props as Props; 13 + 14 + const { keys, locale} = localeKeys; 15 + 16 + const doneCount = keys.filter((key) => key.status == "done").length; 17 + const missingCount = keys.filter((key) => key.status == "missing").length; 18 + const progress: Progress = { 19 + total: doneCount + missingCount, 20 + missing: missingCount, 21 + }; 22 + --- 23 + 24 + <details class="progress-details"> 25 + <summary> 26 + <strong 27 + >{locale.label} ({locale.lang})</strong 28 + > 29 + <br /> 30 + <span class="progress-summary" 31 + >{doneCount} done, {missingCount} missing</span 32 + > 33 + <br /> 34 + <ProgressBar {progress}/> 35 + </summary> 36 + {progress.missing > 0 && 37 + <h3>Missing</h3> 38 + <ul> 39 + {keys.filter((key) => key.status == "missing").map( 40 + (missingKey) => 41 + <li> 42 + <Link text={missingKey.name} href={missingKey.link}/> 43 + </li> 44 + 45 + )} 46 + </ul>} 47 + {progress.missing == 0 && <p>This translation is complete, amazing job! 🎉</p>} 48 + </details> 49 + 50 + <style> 51 + 52 + .progress-details { 53 + margin-bottom: 1.25rem; 54 + } 55 + 56 + details summary { 57 + cursor: pointer; 58 + user-select: none; 59 + } 60 + 61 + details summary:hover strong, 62 + details summary:hover::marker { 63 + color: var(--ln-color-gray-5); 64 + } 65 + 66 + details p { 67 + margin-top: 1.2rem; 68 + } 69 + 70 + details h3 { 71 + margin-top: 1.2rem; 72 + font-size: 0.8rem; 73 + } 74 + 75 + details h4 { 76 + margin-top: 1rem; 77 + font-size: 0.8rem; 78 + } 79 + 80 + details > :last-child { 81 + margin-bottom: 1rem; 82 + } 83 + 84 + .progress-summary { 85 + font-size: 0.8125rem; 86 + } 87 + </style> 88 +
+35
src/components/Overview.astro
··· 1 + --- 2 + import { DashboardData } from "../data"; 3 + 4 + 5 + const pluginLinks = DashboardData.plugins 6 + .map(plugin => ({ 7 + name: plugin.name, 8 + link: `/plugins/${plugin.packageName}`, 9 + })); 10 + 11 + const localeLinks = DashboardData.locales 12 + .map(locale => ({ 13 + label: locale.label, 14 + link: `/languages/${locale.lang}`, 15 + })); 16 + --- 17 + 18 + <h2 id="by-plugin"> 19 + <a href="#by-plugin">View by plugin</a> 20 + </h2> 21 + <ul> 22 + <li><a href="/">View All (Homepage)</a></li> 23 + {pluginLinks.map((plugin) => ( 24 + <li><a href={plugin.link}>{plugin.name}</a></li> 25 + ))} 26 + </ul> 27 + 28 + <h2 id="by-language"> 29 + <a href="#by-language">View by language</a> 30 + </h2> 31 + <ul> 32 + {localeLinks.map((locale) => ( 33 + <li><a href={locale.link}>{locale.label}</a></li> 34 + ))} 35 + </ul>
+59
src/components/ProgressBar.astro
··· 1 + --- 2 + import type { Progress } from '../schemas'; 3 + 4 + interface Props { 5 + progress: Progress 6 + } 7 + 8 + const { 9 + total, 10 + missing, 11 + size = 20, 12 + } = Astro.props.progress; 13 + 14 + const missingSize = Math.round((missing / total) * size); 15 + const doneSize = size - missingSize; 16 + 17 + const getBlocks = (count: number, type: 'done' | 'missing') => { 18 + return Array.from({ length: count }, () => type); 19 + }; 20 + --- 21 + 22 + <div class="progress-bar" aria-hidden="true"> 23 + {getBlocks(doneSize, 'done').map((_, i) => ( 24 + <div class="done-bar" /> 25 + ))} 26 + {getBlocks(missingSize, 'missing').map((_, i) => ( 27 + <div class="missing-bar" /> 28 + ))} 29 + </div> 30 + 31 + <style> 32 + .progress-bar { 33 + display: flex; 34 + flex-direction: row; 35 + margin-top: 0.5rem; 36 + } 37 + 38 + .progress-bar div:first-of-type { 39 + border-radius: 36px 0px 0px 36px; 40 + } 41 + 42 + .progress-bar div:last-of-type { 43 + border-radius: 0px 36px 36px 0px; 44 + } 45 + 46 + .done-bar, 47 + .missing-bar { 48 + width: 1rem; 49 + height: 1rem; 50 + } 51 + 52 + .done-bar { 53 + background-color: var(--ln-color-done); 54 + } 55 + 56 + .missing-bar { 57 + background-color: var(--ln-color-missing); 58 + } 59 + </style>
+129
src/components/StatusByKey.astro
··· 1 + --- 2 + import type { KeyStatus } from '../schemas'; 3 + import Link from './Link.astro'; 4 + 5 + interface Props { 6 + keyStatuses: KeyStatus[] 7 + } 8 + 9 + const { 10 + keyStatuses 11 + } = Astro.props as Props; 12 + 13 + const langs = Array.from( 14 + new Set( 15 + keyStatuses.flatMap((k) => 16 + k.statuses.map((s) => s.locale.lang) 17 + ) 18 + ) 19 + ); 20 + --- 21 + 22 + <h2 id="by-file"> 23 + <a href="#by-file">Translation status by file</a> 24 + </h2> 25 + <div class="table-scroll-wrapper"> 26 + <table class="status-by-file"> 27 + <thead> 28 + <tr> 29 + <th>Key</th> 30 + {langs.map((lang) => ( 31 + <th>{lang}</th> 32 + ))} 33 + </tr> 34 + </thead> 35 + <tbody> 36 + {keyStatuses.map( 37 + (keyStatus) => ( 38 + <tr> 39 + <td><Link text={keyStatus.key.name} href={keyStatus.key.link}/></td> 40 + {keyStatus.statuses.map((status) => ( 41 + <td> 42 + {status.status == "done" && "✔"} 43 + {status.status == "missing" && "❌"} 44 + </td> 45 + ))} 46 + </tr> 47 + ) 48 + )} 49 + </tbody> 50 + </table> 51 + </div> 52 + <sup>❌ Missing ✔ Done</sup> 53 + 54 + <style> 55 + .table-scroll-wrapper { 56 + overflow-x: auto; 57 + max-width: 100%; 58 + margin-bottom: 1rem; 59 + } 60 + 61 + .status-by-file { 62 + margin-bottom: 1rem; 63 + border-collapse: collapse; 64 + border: 1px solid var(--ln-color-table-border); 65 + font-size: 0.8125rem; 66 + column-gap: 64px; 67 + overflow: hidden; 68 + } 69 + 70 + .status-by-file tr:first-of-type td { 71 + padding-top: 0.5rem; 72 + } 73 + 74 + .status-by-file tr:last-of-type td { 75 + padding-bottom: 0.5rem; 76 + } 77 + 78 + .status-by-file tr td:first-of-type { 79 + padding-inline: 1rem; 80 + } 81 + 82 + .status-by-file th { 83 + border-bottom: 1px solid var(--ln-color-table-border); 84 + background: var(--ln-color-table-background); 85 + position: sticky; 86 + top: -1px; 87 + white-space: nowrap; 88 + padding-inline: 0.3rem; 89 + } 90 + 91 + .status-by-file th, 92 + .status-by-file td { 93 + padding-block: 0.2rem; 94 + } 95 + 96 + .status-by-file tbody tr:hover td { 97 + background: var(--ln-color-table-background); 98 + } 99 + 100 + .status-by-file th:first-of-type, 101 + .status-by-file td:first-of-type { 102 + position: sticky; 103 + left: -1px; 104 + z-index: 1; 105 + text-align: left; 106 + padding-inline-start: 1rem; 107 + } 108 + 109 + .status-by-file td:first-of-type { 110 + background: var(--theme-bg); 111 + } 112 + 113 + .status-by-file th:last-of-type, 114 + .status-by-file td:last-of-type { 115 + text-align: center; 116 + padding-inline-end: 1rem; 117 + } 118 + 119 + .status-by-file td:not(:first-of-type) { 120 + min-width: 2rem; 121 + text-align: center; 122 + cursor: default; 123 + } 124 + 125 + .status-by-file td:not(:first-of-type) a { 126 + text-decoration: none; 127 + } 128 + 129 + </style>
+22
src/components/StatusByLocale.astro
··· 1 + --- 2 + import { convertKeyStatusesToLocaleKeys } from '../utils'; 3 + import type { KeyStatus, LocaleKeys } from '../schemas'; 4 + import LocaleDetails from './LocaleDetails.astro'; 5 + 6 + interface Props { 7 + keyStatuses: KeyStatus[] 8 + } 9 + 10 + const { 11 + keyStatuses 12 + } = Astro.props as Props; 13 + 14 + const localeKeys = convertKeyStatusesToLocaleKeys(keyStatuses); 15 + --- 16 + 17 + <h2 id="by-locale"> 18 + <a href="#by-locale">Translation progress by locale</a> 19 + </h2> 20 + {localeKeys.map((locale: LocaleKeys) => ( 21 + <LocaleDetails localeKeys={locale} /> 22 + ))}
+75
src/data.ts
··· 1 + import type { Dashboard } from "./schemas"; 2 + 3 + export const DashboardData: Dashboard = { 4 + plugins: [ 5 + { 6 + name: "Starlight Blog", 7 + packageName: "starlight-blog", 8 + translationFileLink: "https://github.com/HiDeoo/starlight-blog/blob/main/packages/starlight-blog/translations.ts", 9 + translationFileLinkRaw: 10 + "https://raw.githubusercontent.com/HiDeoo/starlight-blog/refs/heads/main/packages/starlight-blog/translations.ts", 11 + }, 12 + { 13 + name: "Starlight Cooler Credit", 14 + packageName: "starlight-cooler-credit", 15 + translationFileLink: 16 + "https://github.com/trueberryless-org/starlight-cooler-credit/blob/main/packages/starlight-cooler-credit/translations.ts", 17 + translationFileLinkRaw: 18 + "https://raw.githubusercontent.com/trueberryless-org/starlight-cooler-credit/refs/heads/main/packages/starlight-cooler-credit/translations.ts", 19 + }, 20 + { 21 + name: "Starlight View Modes", 22 + packageName: "starlight-view-modes", 23 + translationFileLink: 24 + "https://github.com/trueberryless-org/starlight-view-modes/blob/main/packages/starlight-view-modes/translations.ts", 25 + translationFileLinkRaw: 26 + "https://raw.githubusercontent.com/trueberryless-org/starlight-view-modes/refs/heads/main/packages/starlight-view-modes/translations.ts", 27 + }, 28 + { 29 + name: "Starlight Videos", 30 + packageName: "starlight-videos", 31 + translationFileLink: "https://github.com/HiDeoo/starlight-videos/blob/main/packages/starlight-videos/translations.ts", 32 + translationFileLinkRaw: 33 + "https://raw.githubusercontent.com/HiDeoo/starlight-videos/refs/heads/main/packages/starlight-videos/translations.ts", 34 + }, 35 + { 36 + name: "Starlight Kbd", 37 + packageName: "starlight-kbd", 38 + translationFileLink: "https://github.com/HiDeoo/starlight-kbd/blob/main/packages/starlight-kbd/translations.ts", 39 + translationFileLinkRaw: 40 + "https://raw.githubusercontent.com/HiDeoo/starlight-kbd/refs/heads/main/packages/starlight-kbd/translations.ts", 41 + }, 42 + ], 43 + locales: [ 44 + { label: "Čeština", lang: "cs" }, 45 + { label: "Español", lang: "es" }, 46 + { label: "Català", lang: "ca" }, 47 + { label: "Deutsch", lang: "de" }, 48 + { label: "日本語", lang: "ja" }, 49 + { label: "Português", lang: "pt" }, 50 + { label: "فارسی", lang: "fa" }, 51 + { label: "Français", lang: "fr" }, 52 + { label: "Galego", lang: "gl" }, 53 + { label: "עברית", lang: "he" }, 54 + { label: "Bahasa Indonesia", lang: "id" }, 55 + { label: "Italiano", lang: "it" }, 56 + { label: "Nederlands", lang: "nl" }, 57 + { label: "Dansk", lang: "da" }, 58 + { label: "Türkçe", lang: "tr" }, 59 + { label: "العربية", lang: "ar" }, 60 + { label: "Norsk Bokmål", lang: "nb" }, 61 + { label: "中文", lang: "zh" }, 62 + { label: "한국어", lang: "ko" }, 63 + { label: "Svenska", lang: "sv" }, 64 + { label: "Română", lang: "ro" }, 65 + { label: "Русский", lang: "ru" }, 66 + { label: "Tiếng Việt", lang: "vi" }, 67 + { label: "Українська", lang: "uk" }, 68 + { label: "हिन्दी", lang: "hi" }, 69 + { label: "繁體中文", lang: "zh-TW" }, 70 + { label: "Polski", lang: "pl" }, 71 + { label: "Slovenčina", lang: "sk" }, 72 + { label: "Latviešu", lang: "lv" }, 73 + { label: "Magyar", lang: "hu" }, 74 + ], 75 + };
+50
src/layouts/LunariaLayout.astro
··· 1 + --- 2 + import StatusByKey from "../components/StatusByKey.astro"; 3 + import StatusByLocale from "../components/StatusByLocale.astro"; 4 + import { convertToKeyStatuses, type DataPerPlugin } from "../utils"; 5 + import "../styles/globals.css" 6 + import "../styles/variables.css" 7 + import Overview from "../components/Overview.astro"; 8 + 9 + interface Props { 10 + title: string; 11 + pluginsData: DataPerPlugin[] 12 + } 13 + 14 + const { title, pluginsData } = Astro.props as Props; 15 + 16 + const keyStatuses = convertToKeyStatuses(pluginsData) 17 + --- 18 + 19 + <html lang="en"> 20 + <head> <meta charset="utf-8" /> 21 + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" /> 22 + <title>{title}</title> 23 + <meta name="description" content="Tracking translation progress of Starlight plugins UI translations" /> 24 + <meta property="og:title" content={title} /> 25 + <meta property="og:type" content="website" /> 26 + <meta property="og:description" content="Tracking translation progress of Starlight plugins UI translations" /> 27 + 28 + </head> 29 + <body> 30 + <main> 31 + <div class="limit-to-viewport"> 32 + <h1>{title}</h1> 33 + <slot name="description" /> 34 + <StatusByLocale {keyStatuses} /> 35 + </div> 36 + <StatusByKey {keyStatuses} /> 37 + <Overview /> 38 + </main> 39 + </body> 40 + </html> 41 + 42 + <style> 43 + main { 44 + max-width: 80ch; 45 + margin-inline: auto; 46 + } 47 + .limit-to-viewport { 48 + max-width: calc(100vw - 2rem); 49 + } 50 + </style>
+14 -12
src/pages/index.astro
··· 1 1 --- 2 + import LunariaLayout from "../layouts/LunariaLayout.astro"; 3 + import { processPlugins } from "../utils"; 2 4 5 + const pluginsData = await processPlugins(); 3 6 --- 4 7 5 - <html lang="en"> 6 - <head> 7 - <meta charset="utf-8" /> 8 - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 9 - <meta name="viewport" content="width=device-width" /> 10 - <meta name="generator" content={Astro.generator} /> 11 - <title>Astro</title> 12 - </head> 13 - <body> 14 - <h1>Astro</h1> 15 - </body> 16 - </html> 8 + <LunariaLayout title="Starlight Plugins Translation Tracker" {pluginsData}> 9 + <p slot="description"> 10 + If you're interested in helping us translate various <a href="https://starlight.astro.build/resources/plugins/">Starlight plugins</a> that need translations into one of the languages listed below, you've come to the right place! This auto-updating page always lists all the content that could use your help right now. 11 + </p> 12 + <p slot="description"> 13 + In order to translate a missing key into your language, you need to create a new section in the translation file (if you click on the link you get linked to this file) with your language and all the keys strings translated from the <code>"en"</code> section. 14 + </p> 15 + <p slot="description"> 16 + If you are a plugin author that wants to add their plugin to this website, be sure to follow the convention described in <a href="https://github.com/HiDeoo/">@HiDeoo</a>'s <a href="https://hideoo.dev/notes/starlight-plugin-use-custom-translation-strings">blog about translations in Starlight plugins</a> and then you can add your plugin to <a href="#">this list</a>. 17 + </p> 18 + </LunariaLayout>
+43
src/pages/languages/[language].astro
··· 1 + --- 2 + import LunariaLayout from "../../layouts/LunariaLayout.astro"; 3 + import { getLocaleByLang, pluginToLanguageTransformer, processPlugins, type DataPerPlugin } from "../../utils"; 4 + 5 + export async function getStaticPaths() { 6 + const pluginsData = await processPlugins(); 7 + const languageData = await pluginToLanguageTransformer(pluginsData); 8 + return languageData.map(language => ({ 9 + params: { language: language.lang }, 10 + props: { pluginsData } 11 + })); 12 + } 13 + 14 + const { language } = Astro.params; 15 + const { pluginsData } = Astro.props; 16 + const filteredPluginsData: DataPerPlugin[] = pluginsData 17 + .map(plugin => { 18 + const filteredKeys = plugin.keys 19 + .filter(key => key.locales[language]) 20 + .map(key => ({ 21 + key: key.key, 22 + locales: { 23 + [language]: key.locales[language]! 24 + } 25 + })); 26 + 27 + if (filteredKeys.length === 0) return null; 28 + 29 + return { 30 + ...plugin, 31 + keys: filteredKeys 32 + }; 33 + }) 34 + .filter((plugin): plugin is DataPerPlugin => plugin !== null); 35 + 36 + const title="Starlight Plugins " + getLocaleByLang(language).label + " Translation Tracker"; 37 + --- 38 + 39 + <LunariaLayout {title} pluginsData={filteredPluginsData}> 40 + <p slot="description"> 41 + You are viewing the translation tracker for the for the language: {getLocaleByLang(language).label}. 42 + </p> 43 + </LunariaLayout>
+21
src/pages/plugins/[plugin].astro
··· 1 + --- 2 + import LunariaLayout from "../../layouts/LunariaLayout.astro"; 3 + import { processPlugins } from "../../utils"; 4 + 5 + export async function getStaticPaths() { 6 + const pluginsData = await processPlugins(); 7 + return pluginsData.map(plugin => ({ 8 + params: { plugin: plugin.packageName }, 9 + props: { pluginsData: plugin } 10 + })); 11 + } 12 + 13 + const { pluginsData } = Astro.props; 14 + const title = pluginsData.name + " Translation Tracker"; 15 + --- 16 + 17 + <LunariaLayout {title} pluginsData={[pluginsData]}> 18 + <p slot="description"> 19 + You are viewing the translation tracker for the <a href={pluginsData.translationFileLink}>{pluginsData.name}</a> plugin. 20 + </p> 21 + </LunariaLayout>
+65
src/schemas.ts
··· 1 + import { z } from "astro/zod"; 2 + 3 + const StatusSchema = z.enum(["done", "missing"]); 4 + 5 + const PluginSchema = z.object({ 6 + name: z.string().describe("The name of the plugin that will be displayed on the website"), 7 + packageName: z 8 + .string() 9 + .describe("The package name of the plugin as it is published on npm and named on GitHub which will be used for URL slugs"), 10 + translationFileLink: z.string().describe("A link to the translations.ts file of the plugin (preferably GitHub)"), 11 + translationFileLinkRaw: z.string().describe("A link to the raw content of the translations.ts file of the plugin (preferably GitHub)"), 12 + }); 13 + 14 + const LocaleSchema = z.object({ 15 + label: z.string().describe('The label of the locale to show in the status dashboard, e.g. `"English"`, `"Português"`, or `"Español"`'), 16 + lang: z 17 + .string() 18 + .describe( 19 + 'The BCP-47 tag of the locale, both to use in smaller widths and to differentiate regional variants, e.g. `"en-US"` (American English) or `"en-GB"` (British English)' 20 + ), 21 + }); 22 + 23 + const DashboardSchema = z.object({ 24 + plugins: z.array(PluginSchema), 25 + locales: z.array(LocaleSchema), 26 + }); 27 + 28 + const ProgressSchema = z.object({ 29 + total: z.number(), 30 + missing: z.number(), 31 + size: z.number().default(20).optional(), 32 + }); 33 + 34 + const KeySchema = z.object({ 35 + name: z.string(), 36 + link: z.string(), 37 + }); 38 + 39 + const LocaleKeysSchema = z.object({ 40 + locale: LocaleSchema, 41 + keys: z.array( 42 + KeySchema.extend({ 43 + status: StatusSchema, 44 + }) 45 + ), 46 + }); 47 + 48 + const KeyStatusesSchema = z.object({ 49 + key: KeySchema, 50 + statuses: z.array( 51 + z.object({ 52 + locale: LocaleSchema, 53 + status: StatusSchema, 54 + }) 55 + ), 56 + }); 57 + 58 + export type Status = z.infer<typeof StatusSchema>; 59 + export type Plugin = z.infer<typeof PluginSchema>; 60 + export type Locale = z.infer<typeof LocaleSchema>; 61 + export type Dashboard = z.infer<typeof DashboardSchema>; 62 + export type Progress = z.infer<typeof ProgressSchema>; 63 + export type Key = z.infer<typeof KeySchema>; 64 + export type LocaleKeys = z.infer<typeof LocaleKeysSchema>; 65 + export type KeyStatus = z.infer<typeof KeyStatusesSchema>;
+88
src/styles/globals.css
··· 1 + * { 2 + box-sizing: border-box; 3 + margin: 0; 4 + } 5 + 6 + html { 7 + background: var(--ln-color-background); 8 + scrollbar-gutter: stable; 9 + } 10 + 11 + body { 12 + color: var(--ln-color-black); 13 + display: flex; 14 + flex-direction: column; 15 + font-family: var(--ln-font-body); 16 + font-size: 16px; 17 + line-height: 1.5; 18 + margin-block: 2rem; 19 + margin-inline: 1rem; 20 + } 21 + 22 + h1, 23 + h2, 24 + h3, 25 + h4, 26 + h5, 27 + h6 { 28 + color: var(--theme-text-bright); 29 + margin-bottom: 1rem; 30 + font-weight: bold; 31 + line-height: 1.3; 32 + } 33 + 34 + h1, 35 + h2 { 36 + max-width: 40ch; 37 + } 38 + 39 + h1 { 40 + margin-top: 5rem; 41 + font-size: 2.25rem; 42 + font-weight: 900; 43 + } 44 + 45 + h2 { 46 + font-size: 1.875rem; 47 + margin-top: 4rem; 48 + } 49 + 50 + h3, 51 + h4 { 52 + margin-top: 3rem; 53 + } 54 + 55 + h5, 56 + h6 { 57 + margin-top: 2rem; 58 + } 59 + 60 + p + p { 61 + margin-top: 1.25rem; 62 + } 63 + 64 + a { 65 + color: var(--ln-color-link); 66 + text-decoration: none; 67 + } 68 + 69 + p a { 70 + text-decoration: underline; 71 + } 72 + 73 + h2 a { 74 + color: inherit; 75 + } 76 + 77 + a:hover { 78 + text-decoration: underline; 79 + } 80 + 81 + sup { 82 + display: flex; 83 + justify-content: center; 84 + } 85 + 86 + ul { 87 + font-size: 0.875rem; 88 + }
+59
src/styles/variables.css
··· 1 + :root { 2 + /** Fonts */ 3 + --ln-font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; 4 + --ln-font-body: system-ui, var(--ln-font-fallback); 5 + --ln-font-mono: "IBM Plex Mono", Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", 6 + "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; 7 + 8 + /* Light theme colors */ 9 + --ln-color-white: #f9fafb; 10 + --ln-color-gray-1: #f3f4f6; 11 + --ln-color-gray-2: #e5e7eb; 12 + --ln-color-gray-3: #d1d5db; 13 + --ln-color-gray-4: #9ca3af; 14 + --ln-color-gray-5: #6b7280; 15 + --ln-color-gray-6: #4b5563; 16 + --ln-color-gray-7: #374151; 17 + --ln-color-black: #030712; 18 + --ln-color-blue: #3b82f6; 19 + --ln-color-orange: #f97316; 20 + --ln-color-purple: #a855f7; 21 + 22 + --theme-accent: hsl(234, 100%, 87%); 23 + --theme-bg: hsl(223, 13%, 10%); 24 + --theme-table-header: hsl(222, 13%, 16%); 25 + --theme-table-hover: hsl(222, 13%, 16%); 26 + --theme-text: hsl(228, 8%, 77%); 27 + --theme-text-bright: hsl(0, 0%, 100%); 28 + --overlay-blurple: hsla(255, 60%, 60%, 0.2); 29 + 30 + /** Contextual colors */ 31 + --ln-color-background: linear-gradient(215deg, var(--overlay-blurple), transparent 40%), 32 + radial-gradient(var(--overlay-blurple), transparent 40%) no-repeat -60vw -40vh / 105vw 200vh, 33 + radial-gradient(var(--overlay-blurple), transparent 65%) no-repeat 50% calc(100% + 20rem) / 60rem 30rem, var(--theme-bg); 34 + --ln-color-link: var(--theme-accent); 35 + --ln-color-black: var(--theme-text); 36 + --ln-color-done: var(--ln-color-blue); 37 + /* --ln-color-outdated: #ea580c; */ 38 + --ln-color-missing: var(--theme-text-bright); 39 + --ln-color-table-background: var(--theme-table-header); 40 + --ln-color-table-border: var(--theme-table-header); 41 + } 42 + 43 + @media (prefers-color-scheme: dark) { 44 + :root { 45 + /* Dark theme colors */ 46 + --ln-color-white: #030712; 47 + --ln-color-gray-1: #374151; 48 + --ln-color-gray-2: #4b5563; 49 + --ln-color-gray-3: #6b7280; 50 + --ln-color-gray-4: #9ca3af; 51 + --ln-color-gray-5: #d1d5db; 52 + --ln-color-gray-6: #e5e7eb; 53 + --ln-color-gray-7: #f3f4f6; 54 + --ln-color-black: #f9fafb; 55 + --ln-color-blue: #60a5fa; 56 + --ln-color-orange: #fb923c; 57 + --ln-color-purple: #c084fc; 58 + } 59 + }
+169
src/utils.ts
··· 1 + import { DashboardData } from "./data.ts"; 2 + import type { KeyStatus, Locale, LocaleKeys, Status } from "./schemas.ts"; 3 + 4 + type TranslationMap = Record<string, Record<string, string>>; // lang -> key -> value 5 + export type DataPerKey = { 6 + key: string; 7 + locales: Record<string, Status>; 8 + }; 9 + 10 + export type DataPerPlugin = { 11 + name: string; 12 + packageName: string; 13 + translationFileLink: string; 14 + translationFileLinkRaw: string; 15 + keys: DataPerKey[]; 16 + }; 17 + 18 + export type DataPerLanguage = { 19 + lang: string; 20 + statuses: Record<Status, string[]>; 21 + }; 22 + 23 + // Utility to fetch the remote .ts file and parse it 24 + async function fetchTranslationFile(url: string): Promise<TranslationMap> { 25 + const res = await fetch(url); 26 + const text = await res.text(); 27 + 28 + // Extract the JSON-ish object from the file 29 + const match = text.match(/export const Translations\s*=\s*(\{[\s\S]*\});?/); 30 + if (!match) throw new Error("Could not find Translations object in the file"); 31 + 32 + // Safely eval or use Function constructor (sandboxing highly recommended in real prod code) 33 + const translationObject = new Function(`return ${match[1]}`)() as TranslationMap; 34 + return translationObject; 35 + } 36 + 37 + export async function processPlugins(): Promise<DataPerPlugin[]> { 38 + const results: DataPerPlugin[] = []; 39 + 40 + for (const plugin of DashboardData.plugins) { 41 + const data = await fetchTranslationFile(plugin.translationFileLinkRaw); 42 + 43 + const defaultLang = "en"; 44 + const defaultKeys = new Set(Object.keys(data[defaultLang] || {})); 45 + const allLocales = DashboardData.locales.map((l) => l.lang); 46 + 47 + const keysStatus: DataPerKey[] = []; 48 + 49 + for (const key of defaultKeys) { 50 + const localesStatus: Record<string, Status> = {}; 51 + 52 + for (const lang of allLocales) { 53 + if (lang === defaultLang) continue; 54 + 55 + const hasTranslation = !!data[lang]?.[key]; 56 + localesStatus[lang] = hasTranslation ? "done" : "missing"; 57 + } 58 + 59 + keysStatus.push({ key, locales: localesStatus }); 60 + } 61 + 62 + results.push({ 63 + name: plugin.name, 64 + packageName: plugin.packageName, 65 + translationFileLink: plugin.translationFileLink, 66 + translationFileLinkRaw: plugin.translationFileLinkRaw, 67 + keys: keysStatus, 68 + }); 69 + } 70 + 71 + return results; 72 + } 73 + 74 + export function pluginToLanguageTransformer(plugins: DataPerPlugin[]): DataPerLanguage[] { 75 + const languageMap = new Map<string, Record<Status, string[]>>(); 76 + 77 + for (const plugin of plugins) { 78 + for (const { key, locales } of plugin.keys) { 79 + for (const [lang, status] of Object.entries(locales)) { 80 + if (!languageMap.has(lang)) { 81 + languageMap.set(lang, { done: [], missing: [] }); 82 + } 83 + 84 + languageMap.get(lang)![status].push(key); 85 + } 86 + } 87 + } 88 + 89 + const result: DataPerLanguage[] = []; 90 + 91 + for (const [lang, statuses] of languageMap.entries()) { 92 + result.push({ lang, statuses }); 93 + } 94 + 95 + return result; 96 + } 97 + 98 + export function convertToKeyStatuses(data: DataPerPlugin[]): KeyStatus[] { 99 + const keyMap = new Map<string, KeyStatus>(); 100 + 101 + for (const plugin of data) { 102 + for (const entry of plugin.keys) { 103 + if (!keyMap.has(entry.key)) { 104 + keyMap.set(entry.key, { 105 + key: { 106 + name: entry.key, 107 + link: plugin.translationFileLink, 108 + }, 109 + statuses: [], 110 + }); 111 + } 112 + 113 + const keyEntry = keyMap.get(entry.key)!; 114 + 115 + for (const [lang, status] of Object.entries(entry.locales)) { 116 + const locale = getLocaleByLang(lang); 117 + keyEntry.statuses.push({ locale, status }); 118 + } 119 + } 120 + } 121 + 122 + return Array.from(keyMap.values()); 123 + } 124 + 125 + export function getLocaleByLang(lang: string): Locale { 126 + const locale = DashboardData.locales.find((l) => l.lang === lang); 127 + if (!locale) throw new Error(`Locale not found for lang: ${lang}`); 128 + return locale; 129 + } 130 + 131 + export function keyStatusesToLocaleKeys(keyStatuses: KeyStatus[], targetLocale: Locale): LocaleKeys { 132 + return { 133 + locale: targetLocale, 134 + keys: keyStatuses.map((ks) => { 135 + const statusEntry = ks.statuses.find((s) => s.locale.lang === targetLocale.lang); 136 + return { 137 + ...ks.key, 138 + status: statusEntry?.status ?? "missing", // fallback to 'missing' if not found 139 + }; 140 + }), 141 + }; 142 + } 143 + 144 + export function convertKeyStatusesToLocaleKeys(keyStatuses: KeyStatus[]): LocaleKeys[] { 145 + const localeMap = new Map<string, { locale: Locale; keys: { name: string; link: string; status: Status }[] }>(); 146 + 147 + for (const { key, statuses } of keyStatuses) { 148 + for (const { locale, status } of statuses) { 149 + const entry = localeMap.get(locale.lang); 150 + 151 + const keyEntry = { 152 + name: key.name, 153 + link: key.link, 154 + status, 155 + }; 156 + 157 + if (entry) { 158 + entry.keys.push(keyEntry); 159 + } else { 160 + localeMap.set(locale.lang, { 161 + locale, 162 + keys: [keyEntry], 163 + }); 164 + } 165 + } 166 + } 167 + 168 + return Array.from(localeMap.values()); 169 + }