verify-client-ui-i18n.spec.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. import { describe, expect, it } from 'vitest'
  2. import { clientSourceRoot, findUiI18nViolations } from './verify-client-ui-i18n.ts'
  3. function messages(source: string): string[] {
  4. return findUiI18nViolations('packages/client/ui-example/src/client/View.tsx', source)
  5. .map(violation => violation.text)
  6. }
  7. describe('Client UI i18n source check', () => {
  8. it('rejects direct JSX copy and copy-bearing attributes', () => {
  9. expect(messages(`
  10. const View = ({ ready }: { ready: boolean }) => <section aria-label="Overview">
  11. <span>Hard-coded text</span>
  12. <input placeholder={ready ? 'Search now' : ` + "`Wait ${'${ready}'}`" + `} />
  13. <div runningSummary="Still working" />
  14. </section>
  15. `)).toEqual(['Overview', 'Hard-coded text', 'Search now', 'Wait', 'Still working'])
  16. })
  17. it('rejects copy kept in label data and copy helper returns', () => {
  18. expect(messages(`
  19. const TABS = [{ id: 'summary', label: 'Summary' }]
  20. function statusLabel(status: string): string {
  21. if (status === 'done') return 'Complete'
  22. return 'Still running'
  23. }
  24. function duration(): string { return 'Not recorded' }
  25. function mode(): string { return 'compact' }
  26. function displayFailureMessage(): string { return 'API key is invalid' }
  27. const emptySummary = 'Nothing to show'
  28. function Dialog({ closeLabel = 'Close dialog' }: { closeLabel?: string }) { return closeLabel }
  29. `)).toEqual([
  30. 'Summary', 'Complete', 'Still running', 'Not recorded', 'API key is invalid',
  31. 'Nothing to show', 'Close dialog',
  32. ])
  33. })
  34. it('normalizes native separators before deriving a Client source root', () => {
  35. expect(clientSourceRoot('packages/extensions/sample/src/client/View.tsx'))
  36. .toBe('packages/extensions/sample/src/client')
  37. expect(clientSourceRoot('packages\\extensions\\sample\\src\\client\\View.tsx'))
  38. .toBe('packages/extensions/sample/src/client')
  39. expect(clientSourceRoot('packages/extensions/sample/src/server/index.ts')).toBeUndefined()
  40. })
  41. it('accepts translated copy, dynamic values, structural attributes, and language tokens', () => {
  42. expect(messages(`
  43. const View = ({ t, value }: { t: (key: string) => string; value: string }) => (
  44. <section className="root" role="region" aria-label={t('overview')}>
  45. <span>{t('status.complete')}</span>
  46. <code>null</code>
  47. {value === 'pending' && <output>{value}</output>}
  48. <output>{value}</output>
  49. </section>
  50. )
  51. `)).toEqual([])
  52. })
  53. it('does not inspect locale dictionary owners', () => {
  54. expect(findUiI18nViolations(
  55. 'packages/client/ui-example/src/client/locales.ts',
  56. 'export const en = { title: "Hard-coded by design" }',
  57. )).toEqual([])
  58. })
  59. it('rejects Electron dialog, title, prompt, and DOM copy outside locale owners', () => {
  60. const source = `
  61. dialog.showMessageBox({ title: 'Update available', message: 'Install it now?' })
  62. window.setTitle('Desktop plugins')
  63. window.prompt('Target version')
  64. status.textContent = 'Finished'
  65. `
  66. expect(findUiI18nViolations('apps/desktop/src/main.ts', source).map(row => row.text)).toEqual([
  67. 'Update available',
  68. 'Install it now?',
  69. 'Desktop plugins',
  70. 'Target version',
  71. 'Finished',
  72. ])
  73. })
  74. })