parity-script.test.ts 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { describe, it, expect } from 'vitest';
  2. // @ts-expect-error -- .mjs script with no type declarations; pure JS import is fine for tests
  3. import { compareLocales } from '../../../scripts/check-i18n-parity.mjs';
  4. type LocaleMap = Map<string, string>;
  5. const toMap = (obj: Record<string, string>): LocaleMap => new Map(Object.entries(obj));
  6. const hasReport = (
  7. reports: Array<{ label: string; items: string[] }>,
  8. labelSubstr: string,
  9. itemSubstr?: string,
  10. ): boolean =>
  11. reports.some(
  12. (r) =>
  13. r.label.includes(labelSubstr) &&
  14. (itemSubstr === undefined || r.items.some((i) => i.includes(itemSubstr))),
  15. );
  16. describe('compareLocales (parity-script self-test)', () => {
  17. it('passes when all locales match en', () => {
  18. const en = toMap({ 'a.b': 'hello {{name}}', 'count_one': 'one', 'count_other': 'many' });
  19. const result = compareLocales({
  20. en,
  21. 'zh-CN': toMap({ 'a.b': '你好 {{name}}', 'count_one': '一', 'count_other': '多' }),
  22. 'zh-TW': toMap({ 'a.b': '你好 {{name}}', 'count_one': '一', 'count_other': '多' }),
  23. });
  24. expect(result.failed).toBe(false);
  25. expect(result.reports).toEqual([]);
  26. });
  27. it('flags keys missing from a non-en locale', () => {
  28. const result = compareLocales({
  29. en: toMap({ 'a.b': 'x', 'a.c': 'y' }),
  30. 'zh-TW': toMap({ 'a.b': 'x' }),
  31. });
  32. expect(result.failed).toBe(true);
  33. expect(hasReport(result.reports, 'zh-TW: missing keys vs en', 'a.c')).toBe(true);
  34. });
  35. it('flags keys that exist in a non-en locale but not in en', () => {
  36. const result = compareLocales({
  37. en: toMap({ 'a.b': 'x' }),
  38. 'zh-CN': toMap({ 'a.b': 'x', 'a.stray': 'extra' }),
  39. });
  40. expect(result.failed).toBe(true);
  41. expect(hasReport(result.reports, 'zh-CN: extra keys vs en', 'a.stray')).toBe(true);
  42. });
  43. it('flags placeholder mismatch (missing placeholder in translation)', () => {
  44. const result = compareLocales({
  45. en: toMap({ greeting: 'Hello {{name}}!' }),
  46. 'zh-CN': toMap({ greeting: '你好!' }), // {{name}} dropped
  47. });
  48. expect(result.failed).toBe(true);
  49. expect(hasReport(result.reports, 'placeholder mismatch', 'greeting')).toBe(true);
  50. });
  51. it('flags placeholder mismatch (translation introduces unknown placeholder)', () => {
  52. // This is the exact class of bug the zh-CN sync caught:
  53. // fileManager.uploadFailed had a stray {{count}} copied from a sibling key.
  54. const result = compareLocales({
  55. en: toMap({ uploadFailed: 'Upload failed' }),
  56. 'zh-CN': toMap({ uploadFailed: '{{count}} 个失败' }),
  57. });
  58. expect(result.failed).toBe(true);
  59. expect(hasReport(result.reports, 'placeholder mismatch', 'uploadFailed')).toBe(true);
  60. });
  61. it('flags missing plural suffix keys in a non-en locale', () => {
  62. const result = compareLocales({
  63. en: toMap({ item_one: 'item', item_other: 'items' }),
  64. 'zh-TW': toMap({ item_one: '項目' }), // item_other missing
  65. });
  66. expect(result.failed).toBe(true);
  67. expect(hasReport(result.reports, 'plural key mismatch', 'missing _other')).toBe(true);
  68. });
  69. it('flags a non-en _one key that does not exist in en', () => {
  70. const result = compareLocales({
  71. en: toMap({ item: 'item' }),
  72. 'zh-CN': toMap({ item: '项', item_one: '一项' }), // en never plural-gated this
  73. });
  74. expect(result.failed).toBe(true);
  75. expect(
  76. hasReport(result.reports, 'plural key mismatch', 'unexpected _one not present in en'),
  77. ).toBe(true);
  78. });
  79. it('throws when the en locale is absent', () => {
  80. expect(() =>
  81. compareLocales({ 'zh-CN': toMap({ a: 'x' }) } as Record<string, LocaleMap>),
  82. ).toThrow(/locales\.en/);
  83. });
  84. });