Skip to content

feat: improve i18n (lunaria) status page#2064

Merged
ghostdevv merged 12 commits intonpmx-dev:mainfrom
alex-key:feat/improve-lunaria-status-page
Mar 21, 2026
Merged

feat: improve i18n (lunaria) status page#2064
ghostdevv merged 12 commits intonpmx-dev:mainfrom
alex-key:feat/improve-lunaria-status-page

Conversation

@alex-key
Copy link
Contributor

@alex-key alex-key commented Mar 13, 2026

🧭 Context

i18n status page was designed to track multiple files. Since we are using a single file for each location - it could be improved

📚 Description

The following changes were done:

  • use processed and optimized jsonStatus object instead of lunaria-generated status object
  • removed "Translation status by file" section on the bottom as it's not needed when we use a single file per location
  • replaced same file output in each locale ("i18n/locales/en.json") with correct locale filename
  • replaced redundant report per file "0 done, 1 outdated,0 missing" with missing keys count
  • improved progress bar render based on competion %
  • added progress bar color scheme based on completion %
  • general visual improvements: paddings, margins, borders, link hover state
Before

i18n-npmx-old

After

i18n-nuxt-new


UPDATED

After conversation with @userquin integrated his changes from https://github.com/npmx-dev/npmx.dev/pull/1485/changes

  • added a new page /translation-status
  • added links at app footer, mobile menu and settings (at the end of the lang section)
  • re-designed status output including loading state skeletons and new progress bar color scheme
Screenshots image image image
Details

@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 20, 2026 8:42pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 20, 2026 8:42pm
npmx-lunaria Ignored Ignored Mar 20, 2026 8:42pm

Request Review

@codecov
Copy link

codecov bot commented Mar 13, 2026

Codecov Report

❌ Patch coverage is 42.85714% with 16 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/useI18nStatus.ts 0.00% 7 Missing and 2 partials ⚠️
app/components/ProgressBar.vue 50.00% 4 Missing and 2 partials ⚠️
config/i18n.ts 83.33% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@alex-key alex-key marked this pull request as ready for review March 13, 2026 18:00
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

📝 Walkthrough

Walkthrough

The PR migrates Lunaria’s dashboard rendering to consume an I18nStatus shape (including per-locale dir and totalKeys) instead of LunariaStatus, updates the exported Page signature, and defers HTML generation until jsonStatus is built. It adds a translation-status Nuxt page and new UI components/helpers (ProgressBar, MissingKeysList, ContentDetailsLinks, Link, TitleParagraph), updates styles with new progress tokens and rules, extends status JSON output, updates shared i18n status types, and includes small composable, test and config changes (knip, routeRules, middleware, i18n keys/schema).

Possibly related PRs

Suggested reviewers

  • userquin
  • graphieros
  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset by explaining the redesign of the i18n translation status UI, including the shift from file-based to locale-based reporting, removal of the 'by file' section, improvements to progress bars, and visual enhancements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can approve the review once all CodeRabbit's comments are resolved.

Enable the reviews.request_changes_workflow setting to automatically approve the review once all CodeRabbit's comments are resolved.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
lunaria/components.ts (1)

260-261: Avoid ! after find().

If localizations does not contain lang, this throws before any fallback status can be rendered. A small guard keeps this exported helper safe if the per-file view is brought back.

Safer fallback
-  const localization = localizations.find(l => l.lang === lang)!
+  const localization = localizations.find(l => l.lang === lang)
+  if (!localization) {
+    return html`<td>${EmojiFileLink(null, 'missing')}</td>`
+  }
As per coding guidelines, "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8dd22553-934d-4d27-9362-b2a69b3d653e

📥 Commits

Reviewing files that changed from the base of the PR and between a170292 and 326dc61.

📒 Files selected for processing (3)
  • lunaria/components.ts
  • lunaria/lunaria.ts
  • lunaria/styles.ts

@userquin
Copy link
Member

We want that page at npmx.dev, check #1485

@alex-key
Copy link
Contributor Author

We want that page at npmx.dev, check #1485

So we keep both i18n.npmx.dev and /translation-status? I can't check new Vue component precisely: did you change design much?

@alex-key alex-key marked this pull request as draft March 14, 2026 23:05
@alex-key
Copy link
Contributor Author

I'll work to include changes from #1485

@github-actions
Copy link

github-actions bot commented Mar 18, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
i18n/locales/uk-UA.json Localization changed, will be marked as complete. 🔄️
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@alex-key
Copy link
Contributor Author

@userquin Ready for review, thx!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
app/pages/translation-status.vue (1)

189-195: Shared copied state affects all copy buttons.

The copied ref from useClipboard() is shared across all locale entries. When a user copies missing keys for one locale, all "Copy keys" buttons will momentarily display "Copied" text until the state resets.

Consider tracking copied state per locale if distinct feedback is desired.

♻️ Potential approach
const copiedLocale = ref<string | null>(null)

function copyMissingKeys(localeEntry: I18nLocaleStatus) {
  // ... existing copy logic ...
  copy(fullTemplate)
  copiedLocale.value = localeEntry.lang
  setTimeout(() => {
    if (copiedLocale.value === localeEntry.lang) {
      copiedLocale.value = null
    }
  }, 2000)
}

Then in template: copiedLocale === localeEntry.lang ? $t('common.copied') : $t('i18n.copy_keys')

app/components/ProgressBar.vue (1)

105-107: Misplaced RTL styling.

This CSS rule targets details[dir='rtl'] elements with .icon-rtl children, which are not part of this ProgressBar component. Since this is a scoped style block, this rule will never match any elements.

This styling should be moved to the parent component (translation-status.vue) or a shared stylesheet where it can affect the relevant <details> elements.

♻️ Proposed fix - remove from this file
 progress.full::-moz-progress-bar {
   `@apply` bg-green-700 dark:bg-green-500;
 }
-
-details[dir='rtl']:not([open]) .icon-rtl {
-  transform: scale(-1, 1);
-}
 </style>
app/pages/settings.vue (1)

245-246: Use the locale value type in the model update handler (not the Ref type).

Line 246 currently casts $event to typeof currentLocale, which is a Ref, not the emitted locale value. This weakens type-safety and can mask payload mismatches.

Suggested fix
-                  v-model="currentLocale"
-                  `@update`:modelValue="setLocale($event as typeof currentLocale)"
+                  :modelValue="currentLocale"
+                  `@update`:modelValue="setLocale($event as typeof currentLocale.value)"

As per coding guidelines: "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39c3f89f-e1d4-4854-8fb9-b7d8cab8a083

📥 Commits

Reviewing files that changed from the base of the PR and between 326dc61 and bfa664a.

📒 Files selected for processing (18)
  • app/components/AppFooter.vue
  • app/components/ProgressBar.vue
  • app/components/Translation/StatusByFile.unused.vue
  • app/composables/useI18nStatus.ts
  • app/pages/settings.vue
  • app/pages/translation-status.vue
  • config/i18n.ts
  • i18n/locales/en.json
  • i18n/locales/uk-UA.json
  • i18n/schema.json
  • knip.ts
  • lunaria/components.ts
  • lunaria/lunaria.ts
  • nuxt.config.ts
  • server/middleware/canonical-redirects.global.ts
  • shared/types/i18n-status.ts
  • test/nuxt/a11y.spec.ts
  • test/unit/a11y-component-coverage.spec.ts
✅ Files skipped from review due to trivial changes (5)
  • nuxt.config.ts
  • app/components/AppFooter.vue
  • i18n/locales/uk-UA.json
  • knip.ts
  • config/i18n.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lunaria/lunaria.ts

"by_file": "Translation progress by file",
"complete_text": "This translation is complete, amazing job!",
"missing_text": "missing",
"missing_keys": "There is no missing translations | Missing translation | Missing translations",
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

Fix grammar in the zero-count pluralisation string.

The zero-form text is currently ungrammatical (There is no missing translations).

Suggested fix
-    "missing_keys": "There is no missing translations | Missing translation | Missing translations",
+    "missing_keys": "There are no missing translations | Missing translation | Missing translations",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"missing_keys": "There is no missing translations | Missing translation | Missing translations",
"missing_keys": "There are no missing translations | Missing translation | Missing translations",

@userquin
Copy link
Member

userquin commented Mar 19, 2026

Check translation status page, expanding progress in narrow/mobile screens will expand to full width.

Missing translation status link on menu.

@alex-key
Copy link
Contributor Author

Check translation status page, expanding progress in narrow/mobile screens will expand to full width.

Missing translation status link on menu.

Both were fixed

Mobile view image

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
app/pages/translation-status.vue (1)

57-65: Remove inline focus-visible utility class from button.

The focus-visible:outline-accent/70 class on this button should be removed. Focus-visible styling for buttons is handled globally via main.css. Based on learnings: "Do not apply per-element inline utility classes like focus-visible:outline-accent/70 on these elements... Rely on the global rule for consistency and maintainability."

♻️ Proposed fix
          <button
            type="button"
-           class="cursor-pointer inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
+           class="cursor-pointer inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded shrink-0"
            `@click`="router.back()"
            v-if="canGoBack"
          >
lunaria/components.ts (1)

162-180: Clamp progress before computing class and inline width.

Line 179 currently trusts percentComplete as-is. A small defensive clamp avoids invalid widths/classes if upstream values drift outside 0..100.

Suggested refactor
 const ProgressBar = (percentComplete: number): string => {
-  let barClass = 'completed'
+  const safePercent = Number.isFinite(percentComplete)
+    ? Math.min(100, Math.max(0, percentComplete))
+    : 0
+  let barClass = 'completed'
 
-  if (percentComplete > 99) {
+  if (safePercent > 99) {
     barClass = 'completed' // dark-green
-  } else if (percentComplete > 90) {
+  } else if (safePercent > 90) {
     barClass = 'very-good' // green
-  } else if (percentComplete > 75) {
+  } else if (safePercent > 75) {
     barClass = 'good' // orange
-  } else if (percentComplete > 50) {
+  } else if (safePercent > 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 class="progress-bar ${barClass}" style="width:${safePercent}%;"></div>
 		</div>
 	`
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e6b64a2-b1ac-4a01-94a2-395b53a95164

📥 Commits

Reviewing files that changed from the base of the PR and between bfa664a and e61ab49.

📒 Files selected for processing (6)
  • app/components/AppHeader.vue
  • app/pages/translation-status.vue
  • i18n/locales/en.json
  • i18n/schema.json
  • lunaria/components.ts
  • test/nuxt/a11y.spec.ts
✅ Files skipped from review due to trivial changes (2)
  • test/nuxt/a11y.spec.ts
  • i18n/schema.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • i18n/locales/en.json

Comment on lines +27 to +34
const generatedAt = computed(() => {
const gat = status.value?.generatedAt
if (import.meta.client) {
return (nuxt.isHydrated ? new Date().toISOString() : gat) ?? new Date().toISOString()
}

return gat ?? new Date().toISOString()
})
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 | 🟠 Major

Bug: generatedAt returns current time instead of actual generation timestamp after hydration.

When nuxt.isHydrated is true, this returns new Date().toISOString() (current browser time) rather than the actual gat value from the status payload. This displays incorrect information to users about when the translation status was generated.

🐛 Proposed fix
 const generatedAt = computed(() => {
   const gat = status.value?.generatedAt
   if (import.meta.client) {
-    return (nuxt.isHydrated ? new Date().toISOString() : gat) ?? new Date().toISOString()
+    return gat ?? new Date().toISOString()
   }
 
   return gat ?? new Date().toISOString()
 })

If there was a specific reason for showing current time post-hydration (e.g., hydration mismatch avoidance), consider using a client-only rendering approach for the timestamp instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const generatedAt = computed(() => {
const gat = status.value?.generatedAt
if (import.meta.client) {
return (nuxt.isHydrated ? new Date().toISOString() : gat) ?? new Date().toISOString()
}
return gat ?? new Date().toISOString()
})
const generatedAt = computed(() => {
const gat = status.value?.generatedAt
if (import.meta.client) {
return gat ?? new Date().toISOString()
}
return gat ?? new Date().toISOString()
})

Comment on lines +189 to +195
<ButtonBase type="button" size="small" @click="copyMissingKeys(localeEntry)">
{{
copied
? $t('common.copied', {}, { locale: localeEntry.lang })
: $t('i18n.copy_keys', {}, { locale: localeEntry.lang })
}}
</ButtonBase>
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

Shared copied state shows "Copied" on all expanded locales simultaneously.

The copied ref from useClipboard is shared across all locale entries. When a user expands multiple locales and clicks "Copy keys" on one, all expanded entries will display "Copied" text, which is misleading.

Consider tracking the last-copied locale to scope the feedback:

🛠️ Suggested approach
+const lastCopiedLang = shallowRef<string | null>(null)
+
 function copyMissingKeys(localeEntry: I18nLocaleStatus) {
   const template = localeEntry.missingKeys.map(key => `  "${key}": ""`).join(',\n')
   const fullTemplate = `// Missing translations for ${localeEntry.label} (${localeEntry.lang})
 // Add these keys to: i18n/locales/${localeEntry.lang}.json

 ${template}`

   copy(fullTemplate)
+  lastCopiedLang.value = localeEntry.lang
 }

Then in the template, check copied && lastCopiedLang === localeEntry.lang for the button text.

@ghostdevv ghostdevv requested a review from userquin March 20, 2026 20:54
Copy link
Member

@userquin userquin left a comment

Choose a reason for hiding this comment

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

LGTM

Thx ❤️

We can iterate later the progress colors, the gray background on light theme maybe should be more darker and on dark theme more lighter (maybe we can use gray at light on dark theme and viceversa or just use the text color we have for the percentage).

image

We can also remove the padding start in the progress and in the final span:

image

@ghostdevv ghostdevv added this pull request to the merge queue Mar 21, 2026
Merged via the queue into npmx-dev:main with commit d2b21b1 Mar 21, 2026
20 checks passed
@github-actions github-actions bot mentioned this pull request Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants