Ver Fonte

Write a readable audit entry for a spreadsheet import (#536)

* Write a readable audit entry for a spreadsheet import

* Count the created records in the sentence itself, so earlier entries read too
Bernt Christian Egeland há 1 dia atrás
pai
commit
eea367b9fd

+ 1 - 1
messages/de/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Arbeitsplatz „{name}“ aktualisiert",
     "work_bay_delete": "Arbeitsplatz {id} gelöscht",
     "settings_setCustomFieldPlacement": "Hat geändert, wo ein benutzerdefiniertes Feld gedruckt wird",
-    "import_commit": "Tabelle importiert: {count} Datensätze angelegt",
+    "import_commit": "Tabelle importiert: {created} Datensätze angelegt",
     "import_undo": "Tabellenimport rückgängig gemacht: {records} Datensätze entfernt",
     "integration_connect": "{name} verbunden",
     "integration_disconnect": "{name} getrennt",

+ 1 - 1
messages/en/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Updated work bay \"{name}\"",
     "work_bay_delete": "Deleted work bay {id}",
     "settings_setCustomFieldPlacement": "Changed where a custom field prints",
-    "import_commit": "Imported a spreadsheet: {count} records created",
+    "import_commit": "Imported a spreadsheet: {created} records created",
     "import_undo": "Undid a spreadsheet import: {records} records removed",
     "integration_connect": "Connected {name}",
     "integration_disconnect": "Disconnected {name}",

+ 1 - 1
messages/es/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Puesto de trabajo «{name}» actualizado",
     "work_bay_delete": "Puesto de trabajo {id} eliminado",
     "settings_setCustomFieldPlacement": "Cambió dónde se imprime un campo personalizado",
-    "import_commit": "Importó una hoja de cálculo: {count} registros creados",
+    "import_commit": "Importó una hoja de cálculo: {created} registros creados",
     "import_undo": "Deshizo una importación de hoja de cálculo: {records} registros eliminados",
     "integration_connect": "{name} conectado",
     "integration_disconnect": "{name} desconectado",

+ 1 - 1
messages/fr/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Poste de travail « {name} » mis à jour",
     "work_bay_delete": "Poste de travail {id} supprimé",
     "settings_setCustomFieldPlacement": "A modifié l'emplacement d'impression d'un champ personnalisé",
-    "import_commit": "Feuille de calcul importée : {count} enregistrements créés",
+    "import_commit": "Feuille de calcul importée : {created} enregistrements créés",
     "import_undo": "Import de feuille de calcul annulé : {records} enregistrements supprimés",
     "integration_connect": "{name} connecté",
     "integration_disconnect": "{name} déconnecté",

+ 1 - 1
messages/it/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Postazione «{name}» aggiornata",
     "work_bay_delete": "Postazione {id} eliminata",
     "settings_setCustomFieldPlacement": "Ha modificato dove viene stampato un campo personalizzato",
-    "import_commit": "Foglio di calcolo importato: {count} record creati",
+    "import_commit": "Foglio di calcolo importato: {created} record creati",
     "import_undo": "Importazione da foglio di calcolo annullata: {records} record rimossi",
     "integration_connect": "{name} collegato",
     "integration_disconnect": "{name} scollegato",

+ 1 - 1
messages/lt/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Atnaujinta darbo vieta „{name}“",
     "work_bay_delete": "Ištrinta darbo vieta {id}",
     "settings_setCustomFieldPlacement": "Pakeitė, kur spausdinamas pasirinktinis laukas",
-    "import_commit": "Importuota skaičiuoklė: sukurta įrašų: {count}",
+    "import_commit": "Importuota skaičiuoklė: sukurta įrašų: {created}",
     "import_undo": "Atšauktas skaičiuoklės importas: pašalinta įrašų: {records}",
     "integration_connect": "Prijungta {name}",
     "integration_disconnect": "Atjungta {name}",

+ 1 - 1
messages/nb/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Oppdaterte arbeidsplass «{name}»",
     "work_bay_delete": "Slettet arbeidsplass {id}",
     "settings_setCustomFieldPlacement": "Endret hvor et egendefinert felt skrives ut",
-    "import_commit": "Importerte et regneark: {count} poster opprettet",
+    "import_commit": "Importerte et regneark: {created} poster opprettet",
     "import_undo": "Angret en regnearkimport: {records} poster fjernet",
     "integration_connect": "Koblet til {name}",
     "integration_disconnect": "Koblet fra {name}",

+ 1 - 1
messages/nl/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Werkplek \"{name}\" bijgewerkt",
     "work_bay_delete": "Werkplek {id} verwijderd",
     "settings_setCustomFieldPlacement": "Wijzigde waar een aangepast veld wordt afgedrukt",
-    "import_commit": "Spreadsheet geïmporteerd: {count} records aangemaakt",
+    "import_commit": "Spreadsheet geïmporteerd: {created} records aangemaakt",
     "import_undo": "Spreadsheetimport ongedaan gemaakt: {records} records verwijderd",
     "integration_connect": "{name} gekoppeld",
     "integration_disconnect": "{name} losgekoppeld",

+ 1 - 1
messages/pl/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Zaktualizowano stanowisko „{name}”",
     "work_bay_delete": "Usunięto stanowisko {id}",
     "settings_setCustomFieldPlacement": "Zmieniono miejsce drukowania pola niestandardowego",
-    "import_commit": "Zaimportowano arkusz: utworzono {count} rekordów",
+    "import_commit": "Zaimportowano arkusz: utworzono {created} rekordów",
     "import_undo": "Cofnięto import arkusza: usunięto {records} rekordów",
     "integration_connect": "Połączono {name}",
     "integration_disconnect": "Rozłączono {name}",

+ 1 - 1
messages/pt-BR/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Baia \"{name}\" atualizada",
     "work_bay_delete": "Baia {id} excluída",
     "settings_setCustomFieldPlacement": "Alterou onde um campo personalizado é impresso",
-    "import_commit": "Importou uma planilha: {count} registros criados",
+    "import_commit": "Importou uma planilha: {created} registros criados",
     "import_undo": "Desfez uma importação de planilha: {records} registros removidos",
     "integration_connect": "{name} conectado",
     "integration_disconnect": "{name} desconectado",

+ 1 - 1
messages/pt-PT/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Posto de trabalho \"{name}\" atualizado",
     "work_bay_delete": "Posto de trabalho {id} eliminado",
     "settings_setCustomFieldPlacement": "Alterou onde um campo personalizado é impresso",
-    "import_commit": "Importou uma folha de cálculo: {count} registos criados",
+    "import_commit": "Importou uma folha de cálculo: {created} registos criados",
     "import_undo": "Anulou uma importação de folha de cálculo: {records} registos removidos",
     "integration_connect": "{name} ligado",
     "integration_disconnect": "{name} desligado",

+ 1 - 1
messages/ru/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "Обновлён пост «{name}»",
     "work_bay_delete": "Удалён пост {id}",
     "settings_setCustomFieldPlacement": "Изменено место печати пользовательского поля",
-    "import_commit": "Импортирована таблица, создано записей: {count}",
+    "import_commit": "Импортирована таблица, создано записей: {created}",
     "import_undo": "Отменён импорт таблицы, удалено записей: {records}",
     "integration_connect": "Подключена интеграция {name}",
     "integration_disconnect": "Отключена интеграция {name}",

+ 1 - 1
messages/tr/audit.json

@@ -327,7 +327,7 @@
     "work_bay_update": "\"{name}\" çalışma alanı güncellendi",
     "work_bay_delete": "{id} çalışma alanı silindi",
     "settings_setCustomFieldPlacement": "Bir özel alanın yazdırıldığı yer değiştirildi",
-    "import_commit": "Bir hesap tablosu içe aktarıldı: {count} kayıt oluşturuldu",
+    "import_commit": "Bir hesap tablosu içe aktarıldı: {created} kayıt oluşturuldu",
     "import_undo": "Bir hesap tablosu içe aktarması geri alındı: {records} kayıt kaldırıldı",
     "integration_connect": "{name} bağlandı",
     "integration_disconnect": "{name} bağlantısı kesildi",

+ 86 - 0
src/__tests__/i18n/audit-summaries.test.ts

@@ -50,6 +50,73 @@ function keysInUse(): { file: string; key: string }[] {
   return found
 }
 
+/** The value names an ICU sentence asks for, from its syntax tree. */
+function namesIn(text: string): Set<string> {
+  const names = new Set<string>()
+  const walk = (elements: unknown[]) => {
+    for (const el of elements as {
+      type: number
+      value?: string
+      options?: Record<string, { value: unknown[] }>
+    }[]) {
+      // 1 argument, 2 number, 3 date, 4 time, 5 select, 6 plural
+      if (el.type >= 1 && el.type <= 6 && el.value) names.add(el.value)
+      for (const option of Object.values(el.options ?? {})) walk(option.value)
+    }
+  }
+  walk(new IntlMessageFormat(text, 'en').getAst() as unknown[])
+  return names
+}
+
+/** The top-level keys of each `details: { key, params: { ... } }` literal. */
+function paramsInUse(): { file: string; key: string; params: Set<string> }[] {
+  const found: { file: string; key: string; params: Set<string> }[] = []
+  const walk = (dir: string) => {
+    for (const entry of readdirSync(dir, { withFileTypes: true })) {
+      const full = path.join(dir, entry.name)
+      if (entry.isDirectory()) {
+        if (entry.name !== '__tests__') walk(full)
+        continue
+      }
+      if (!entry.name.endsWith('.ts') && !entry.name.endsWith('.tsx')) continue
+      const source = readFileSync(full, 'utf-8')
+      for (const m of source.matchAll(
+        /details:\s*\{\s*key:\s*['"]([a-zA-Z_]+)['"]\s*,\s*params:\s*\{/g
+      )) {
+        // The params object, braces balanced; spread objects cannot be read here.
+        let depth = 1
+        let i = (m.index ?? 0) + m[0].length
+        const start = i
+        while (depth > 0 && i < source.length) {
+          if (source[i] === '{') depth++
+          else if (source[i] === '}') depth--
+          i++
+        }
+        const body = source.slice(start, i - 1)
+        if (body.includes('...')) continue
+        const params = new Set<string>()
+        let level = 0
+        let token = ''
+        for (const ch of `${body},`) {
+          if ('{[('.includes(ch)) level++
+          if ('}])'.includes(ch)) level--
+          if (ch === ',' && level === 0) {
+            const name = token
+              .replace(/\/\/.*$/gm, '')
+              .trim()
+              .match(/^([a-zA-Z_]\w*)/)
+            if (name) params.add(name[1])
+            token = ''
+          } else token += ch
+        }
+        found.push({ file: full, key: m[1], params })
+      }
+    }
+  }
+  walk('src')
+  return found
+}
+
 describe('audit summaries', () => {
   it('finds the call sites at all', () => {
     // Without this the sweeps below pass by inspecting nothing.
@@ -68,6 +135,24 @@ describe('audit summaries', () => {
     expect(orphans, `no English for:\n${orphans.join('\n')}`).toEqual([])
   })
 
+  it('passes every value its sentence names', () => {
+    // The render test below fills in every name it knows, so a call site that
+    // passes the wrong ones still renders there. On the audit page the
+    // sentence then fails and the row shows its key: the spreadsheet import
+    // sent created and updated to a sentence that counts {count}.
+    const en = summary('en')
+    const sites = paramsInUse()
+    expect(sites.length).toBeGreaterThan(20)
+    const wrong = sites
+      .filter((site) => en[site.key])
+      .flatMap((site) =>
+        [...namesIn(en[site.key])]
+          .filter((name) => !site.params.has(name))
+          .map((name) => `${site.key} (${site.file}) does not pass {${name}}`)
+      )
+    expect(wrong, wrong.join('\n')).toEqual([])
+  })
+
   it('uses every key it defines', () => {
     // A key nothing writes is either a typo at the call site or wording left
     // behind by a rename. Both are worth knowing about.
@@ -124,6 +209,7 @@ describe('audit summaries', () => {
       type: 'wash_tires',
       count: 2,
       records: 2,
+      created: 2,
       quotes: 2,
       technician: 'Kari',
       job: 'Brakes',