Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 98 additions & 184 deletions lunaria/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type StatusEntry,
} from '@lunariajs/core'
import { BaseStyles, CustomStyles } from './styles.ts'
import type { I18nStatus } from '../shared/types/i18n-status.ts'

export function html(
strings: TemplateStringsArray,
Expand Down Expand Up @@ -33,8 +34,8 @@ function collapsePath(path: string) {

export const Page = (
config: LunariaConfig,
status: LunariaStatus,
lunaria: LunariaInstance,
status: I18nStatus,
_lunaria: LunariaInstance, // currenly not in use
): string => {
return html`
<!doctype html>
Expand All @@ -43,7 +44,7 @@ export const Page = (
${Meta} ${BaseStyles} ${CustomStyles}
</head>
<body>
${Body(config, status, lunaria)}
${Body(config, status)}
</body>
</html>
`
Expand Down Expand Up @@ -73,144 +74,135 @@ export const Meta = html`
<link rel="icon" href="https://npmx.dev/favicon.svg" type="image/svg+xml" />
`

export const Body = (
config: LunariaConfig,
status: LunariaStatus,
lunaria: LunariaInstance,
): string => {
export const Body = (config: LunariaConfig, status: I18nStatus): string => {
return html`
<main>
<div class="limit-to-viewport">
<h1>npmx Translation Status</h1>
${TitleParagraph} ${StatusByLocale(config, status, lunaria)}
${TitleParagraph} ${StatusByLocale(config, status)}
</div>
${StatusByFile(config, status, lunaria)}
</main>
`
}

export const StatusByLocale = (
config: LunariaConfig,
status: LunariaStatus,
lunaria: LunariaInstance,
): string => {
export const StatusByLocale = (config: LunariaConfig, status: I18nStatus): string => {
const { locales } = config
return html`
<h2 id="by-locale">
<a href="#by-locale">Translation progress by locale</a>
</h2>
${locales.map(locale => LocaleDetails(status, locale, lunaria))}
${locales.map(locale => LocaleDetails(status, locale))}
`
}

export const LocaleDetails = (
status: LunariaStatus,
locale: Locale,
lunaria: LunariaInstance,
): string => {
export const LocaleDetails = (status: I18nStatus, locale: Locale): string => {
const { label, lang } = locale
const localeStatus = status.locales.find(s => s.lang === lang)

const missingFiles = status.filter(
file =>
file.localizations.find(localization => localization.lang === lang)?.status === 'missing',
)
const outdatedFiles = status.filter(file => {
const localization = file.localizations.find(localization => localization.lang === lang)

if (!localization || localization.status === 'missing') return false
if (file.type === 'dictionary')
return 'missingKeys' in localization ? localization.missingKeys.length > 0 : false

return (
localization.status === 'outdated' ||
('missingKeys' in localization && localization.missingKeys.length > 0)
)
})

const doneLength = status.length - outdatedFiles.length - missingFiles.length
if (!localeStatus) {
return ''
}

const links = lunaria.gitHostingLinks()
const {
missingKeys,
percentComplete,
totalKeys,
completedKeys,
githubEditUrl,
githubHistoryUrl,
} = localeStatus

return html`
<details class="progress-details">
<summary>
<strong>${label} (${lang})</strong>
<br />
<span class="progress-summary">
${doneLength.toString()} done, ${outdatedFiles.length.toString()} outdated,
${missingFiles.length.toString()} missing
</span>
<br />
${ProgressBar(status.length, outdatedFiles.length, missingFiles.length)}
<strong>${label} <span class="lang-code">${lang}</span></strong>
<hr />
<div class="progress-summary">
<span>
${missingKeys.length ? `${missingKeys.length.toString()} missing keys` : '✔'}
</span>
<span>${completedKeys} / ${totalKeys}</span>
</div>
${ProgressBar(percentComplete)}
</summary>
${outdatedFiles.length > 0 ? OutdatedFiles(outdatedFiles, lang, lunaria) : ''}
<br />
${ContentDetailsLinks({ text: `i18n/locales/${lang}.json`, url: githubEditUrl }, githubHistoryUrl)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Render the resolved locale filename here.

The edit/history URLs are already built from the resolved path, but this label is reconstructed from lang. For locales whose primary edit target is a base file, the page will still say i18n/locales/ar-EG.json or i18n/locales/es-ES.json even though the link opens ar.json or es.json. Please carry the resolved file path through I18nStatus and render that instead.

<br />
<br />
${
missingFiles.length > 0
? html`<h3 class="capitalize">Missing</h3>
<ul>
${missingFiles.map(file => {
const localization = file.localizations.find(
localization => localization.lang === lang,
)!
return html`
<li>
${Link(links.source(file.source.path), collapsePath(file.source.path))}
${CreateFileLink(links.create(localization.path), 'Create file')}
</li>
`
})}
</ul>`
: ''
}
${
missingFiles.length == 0 && outdatedFiles.length == 0
? html`
missingKeys.length > 0
? html`${MissingKeysList(missingKeys)}`
: html`
<p>This translation is complete, amazing job! 🎉</p>
`
: ''
}
</details>
`
}

export const OutdatedFiles = (
outdatedFiles: LunariaStatus,
lang: string,
lunaria: LunariaInstance,
export const MissingKeysList = (missingKeys: string[]): string => {
return html`<details>
<summary>Show missing keys</summary>
<ul>
${missingKeys.map(key => html`<li>${key}</li>`)}
</ul>
</details>`
}

export const ContentDetailsLinks = (
githubEditLink: { text: string; url: string },
githubHistoryUrl: string,
): string => {
return html`
<h3 class="capitalize">Outdated</h3>
<ul>
${outdatedFiles.map(file => {
const localization = file.localizations.find(localization => localization.lang === lang)!

const isMissingKeys =
localization.status !== 'missing' &&
'missingKeys' in localization &&
localization.missingKeys.length > 0

return html`
<li>
${
isMissingKeys
? html`
<details>
<summary>${ContentDetailsLinks(file, lang, lunaria)}</summary>
<h4>Missing keys</h4>
<ul>
${localization.missingKeys.map(key => html`<li>${(key as unknown as string[]).join('.')}</li>`)}
</ul>
</details>
`
: html` ${ContentDetailsLinks(file, lang, lunaria)} `
}
</li>
`
})}
</ul>
${Link(githubEditLink.url, githubEditLink.text)} |
${Link(githubHistoryUrl, 'source change history')}
`
}

export const ProgressBar = (percentComplete: number): string => {
let barClass = 'completed'

if (percentComplete > 99) {
barClass = 'completed' // dark-green
} else if (percentComplete > 90) {
barClass = 'very-good' // green
} else if (percentComplete > 75) {
barClass = 'good' // orange
} else if (percentComplete > 50) {
barClass = 'help-needed' // red
} else {
barClass = 'basic' // dark-red
}

return html`
<div class="progress-bar-wrapper" aria-hidden="true">
<div class="progress-bar ${barClass}" style="width:${percentComplete}%;"></div>
</div>
`
}

export const Link = (href: string, text: string): string => {
return html`<a href="${href}" target="_blank">${text}</a>`
}

export const TitleParagraph = html`
<p>
If you're interested in helping us translate
<a href="https://npmx.dev/">npmx.dev</a> 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.
</p>
<p>
Before starting, please read our
<a href="https://github.com/npmx-dev/npmx.dev/blob/main/CONTRIBUTING.md#localization-i18n"
>localization (i18n) guide</a
>
to learn about our translation process and how you can get involved.
</p>
`

// Components from here are not used at the moment
// Do not delete as we might use it if we split translations in multiple files for locale
export const StatusByFile = (
config: LunariaConfig,
status: LunariaStatus,
Expand Down Expand Up @@ -265,7 +257,7 @@ export const TableContentStatus = (
lunaria: LunariaInstance,
fileType?: string,
): string => {
const localization = localizations.find(localization => localization.lang === lang)!
const localization = localizations.find(l => l.lang === lang)!
const isMissingKeys = 'missingKeys' in localization && localization.missingKeys.length > 0
// For dictionary files, status is determined solely by key completion:
// if there are missing keys it's "outdated", if all keys are present it's "up-to-date",
Expand All @@ -287,37 +279,6 @@ export const TableContentStatus = (
return html`<td>${EmojiFileLink(link, status)}</td>`
}

export const ContentDetailsLinks = (
fileStatus: StatusEntry,
lang: string,
lunaria: LunariaInstance,
): string => {
const localization = fileStatus.localizations.find(localization => localization.lang === lang)!
const isMissingKeys =
localization.status !== 'missing' &&
'missingKeys' in localization &&
localization.missingKeys.length > 0

const links = lunaria.gitHostingLinks()

return html`
${Link(links.source(fileStatus.source.path), collapsePath(fileStatus.source.path))}
(${Link(
links.source(localization.path),
isMissingKeys ? 'incomplete translation' : 'outdated translation',
)},
${Link(
links.history(
fileStatus.source.path,
'git' in localization
? new Date(localization.git.latestTrackedCommit.date).toISOString()
: undefined,
),
'source change history',
)})
`
}

export const EmojiFileLink = (
href: string | null,
type: 'missing' | 'outdated' | 'up-to-date',
Expand All @@ -343,56 +304,10 @@ export const EmojiFileLink = (
</span>`
}

export const Link = (href: string, text: string): string => {
return html`<a href="${href}">${text}</a>`
}

export const CreateFileLink = (href: string, text: string): string => {
return html`<a class="create-button" href="${href}">${text}</a>`
}

export const ProgressBar = (
total: number,
outdated: number,
missing: number,
{ size = 20 }: { size?: number } = {},
): string => {
const outdatedSize = Math.round((outdated / total) * size)
const missingSize = Math.round((missing / total) * size)
const doneSize = size - outdatedSize - missingSize

const getBlocks = (size: number, type: 'missing' | 'outdated' | 'up-to-date') => {
const items = []
for (let i = 0; i < size; i++) {
items.push(html`<div class="${type}-bar"></div>`)
}
return items
}

return html`
<div class="progress-bar" aria-hidden="true">
${getBlocks(doneSize, 'up-to-date')} ${getBlocks(outdatedSize, 'outdated')}
${getBlocks(missingSize, 'missing')}
</div>
`
}

export const TitleParagraph = html`
<p>
If you're interested in helping us translate
<a href="https://npmx.dev/">npmx.dev</a> 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.
</p>
<p>
Before starting, please read our
<a href="https://github.com/npmx-dev/npmx.dev/blob/main/CONTRIBUTING.md#localization-i18n"
>localization (i18n) guide</a
>
to learn about our translation process and how you can get involved.
</p>
`

/**
* Build an SVG file showing a summary of each language's translation progress.
*/
Expand Down Expand Up @@ -421,11 +336,10 @@ function SvgLocaleSummary(
{ label, lang }: Locale,
): { svg: string; progress: number } {
const missingFiles = status.filter(
file =>
file.localizations.find(localization => localization.lang === lang)?.status === 'missing',
file => file.localizations.find(l => l.lang === lang)?.status === 'missing',
)
const outdatedFiles = status.filter(file => {
const localization = file.localizations.find(localization => localization.lang === lang)
const localization = file.localizations.find(l => l.lang === lang)
if (!localization || localization.status === 'missing') {
return false
} else if (file.type === 'dictionary') {
Expand Down
Loading
Loading