diff --git a/apps/mark-scan/backend/schema.sql b/apps/mark-scan/backend/schema.sql index f5600df7a7..649b01a668 100644 --- a/apps/mark-scan/backend/schema.sql +++ b/apps/mark-scan/backend/schema.sql @@ -12,3 +12,27 @@ create table system_settings ( id integer primary key check (id = 1), data text not null -- JSON blob ); + +create table languages ( + code text primary key +); + +create table ui_strings ( + language_code text primary key, + data text not null, -- JSON blob - see libs/types/UiStringTranslationsSchema + foreign key (language_code) references languages(code) +); + +create table audio_clips ( + id text not null, + language_code text not null, + data_base64 text not null, -- Base64-encoded audio bytes + primary key (language_code, id), + foreign key (language_code) references languages(code) +); + +create table ui_string_audio_keys ( + language_code text primary key, + data text not null, -- JSON blob - see libs/types/UiStringAudioKeysSchema + foreign key (language_code) references languages(code) +); diff --git a/apps/mark-scan/backend/src/app.test.ts b/apps/mark-scan/backend/src/app.test.ts index 573a52cb49..13db44e4f5 100644 --- a/apps/mark-scan/backend/src/app.test.ts +++ b/apps/mark-scan/backend/src/app.test.ts @@ -62,6 +62,7 @@ beforeEach(async () => { afterEach(() => { stateMachine.stopMachineService(); server?.close(); + jest.resetAllMocks(); }); async function setUpUsbAndConfigureElection( diff --git a/apps/mark-scan/backend/src/app.ts b/apps/mark-scan/backend/src/app.ts index 1c80dddd1c..e6033e4102 100644 --- a/apps/mark-scan/backend/src/app.ts +++ b/apps/mark-scan/backend/src/app.ts @@ -20,7 +20,11 @@ import { singlePrecinctSelectionFor, } from '@votingworks/utils'; -import { Usb, readBallotPackageFromUsb } from '@votingworks/backend'; +import { + createUiStringsApi, + Usb, + readBallotPackageFromUsb, +} from '@votingworks/backend'; import { Logger } from '@votingworks/logging'; import { electionGeneralDefinition } from '@votingworks/fixtures'; import { useDevDockRouter } from '@votingworks/dev-dock-backend'; @@ -36,7 +40,8 @@ const debug = makeDebug('mark-scan:app-backend'); const defaultMediaMountDir = '/media'; -function buildApi( +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function buildApi( auth: InsertedSmartCardAuthApi, usb: Usb, logger: Logger, @@ -232,6 +237,11 @@ function buildApi( stateMachine.confirmInvalidateBallot(); }, + + ...createUiStringsApi({ + logger, + store: workspace.store.getUiStringsStore(), + }), }); } diff --git a/apps/mark-scan/backend/src/app.ui_strings.test.ts b/apps/mark-scan/backend/src/app.ui_strings.test.ts new file mode 100644 index 0000000000..cc4fdc1d6f --- /dev/null +++ b/apps/mark-scan/backend/src/app.ui_strings.test.ts @@ -0,0 +1,26 @@ +import tmp from 'tmp'; + +import { createMockUsb, runUiStringApiTests } from '@votingworks/backend'; +import { fakeLogger } from '@votingworks/logging'; + +import { buildMockInsertedSmartCardAuth } from '@votingworks/auth'; +import { Store } from './store'; +import { createWorkspace } from './util/workspace'; +import { buildApi } from './app'; + +const store = Store.memoryStore(); +const workspace = createWorkspace(tmp.dirSync().name, { store }); + +afterAll(() => { + workspace.reset(); +}); + +runUiStringApiTests({ + api: buildApi( + buildMockInsertedSmartCardAuth(), + createMockUsb().mock, + fakeLogger(), + workspace + ), + store: store.getUiStringsStore(), +}); diff --git a/apps/mark-scan/backend/src/store.ts b/apps/mark-scan/backend/src/store.ts index 553823868c..a3a615e109 100644 --- a/apps/mark-scan/backend/src/store.ts +++ b/apps/mark-scan/backend/src/store.ts @@ -2,6 +2,7 @@ // The durable datastore for configuration info. // +import { UiStringsStore, createUiStringStore } from '@votingworks/backend'; import { Optional } from '@votingworks/basics'; import { Client as DbClient } from '@votingworks/db'; import { @@ -21,7 +22,10 @@ const SchemaPath = join(__dirname, '../schema.sql'); * Manages a data store for imported election definition and system settings */ export class Store { - private constructor(private readonly client: DbClient) {} + private constructor( + private readonly client: DbClient, + private readonly uiStringsStore: UiStringsStore + ) {} getDbPath(): string { return this.client.getDatabasePath(); @@ -31,14 +35,18 @@ export class Store { * Builds and returns a new store whose data is kept in memory. */ static memoryStore(): Store { - return new Store(DbClient.memoryClient(SchemaPath)); + const client = DbClient.memoryClient(SchemaPath); + const uiStringsStore = createUiStringStore(client); + return new Store(client, uiStringsStore); } /** * Builds and returns a new store at `dbPath`. */ static fileStore(dbPath: string): Store { - return new Store(DbClient.fileClient(dbPath, SchemaPath)); + const client = DbClient.fileClient(dbPath, SchemaPath); + const uiStringsStore = createUiStringStore(client); + return new Store(client, uiStringsStore); } /** @@ -184,4 +192,8 @@ export class Store { if (!result) return undefined; return safeParseSystemSettings(result.data).unsafeUnwrap(); } + + getUiStringsStore(): UiStringsStore { + return this.uiStringsStore; + } } diff --git a/apps/mark-scan/backend/src/util/workspace.ts b/apps/mark-scan/backend/src/util/workspace.ts index d3a5436d0a..7ce8988205 100644 --- a/apps/mark-scan/backend/src/util/workspace.ts +++ b/apps/mark-scan/backend/src/util/workspace.ts @@ -36,12 +36,15 @@ export function constructAuthMachineState( }; } -export function createWorkspace(root: string): Workspace { +export function createWorkspace( + root: string, + options: { store?: Store } = {} +): Workspace { const resolvedRoot = resolve(root); ensureDirSync(resolvedRoot); const dbPath = join(resolvedRoot, 'mark.db'); - const store = Store.fileStore(dbPath); + const store = options.store || Store.fileStore(dbPath); return { path: resolvedRoot, diff --git a/apps/mark/backend/schema.sql b/apps/mark/backend/schema.sql index f5de6718a2..a275fabdc0 100644 --- a/apps/mark/backend/schema.sql +++ b/apps/mark/backend/schema.sql @@ -11,3 +11,27 @@ create table system_settings ( id integer primary key check (id = 1), data text not null -- JSON blob ); + +create table languages ( + code text primary key +); + +create table ui_strings ( + language_code text primary key, + data text not null, -- JSON blob - see libs/types/UiStringTranslationsSchema + foreign key (language_code) references languages(code) +); + +create table audio_clips ( + id text not null, + language_code text not null, + data_base64 text not null, -- Base64-encoded audio bytes + primary key (language_code, id), + foreign key (language_code) references languages(code) +); + +create table ui_string_audio_keys ( + language_code text primary key, + data text not null, -- JSON blob - see libs/types/UiStringAudioKeysSchema + foreign key (language_code) references languages(code) +); diff --git a/apps/mark/backend/src/app.ts b/apps/mark/backend/src/app.ts index f1adaff7cb..3a588444cc 100644 --- a/apps/mark/backend/src/app.ts +++ b/apps/mark/backend/src/app.ts @@ -16,7 +16,11 @@ import { } from '@votingworks/types'; import { isElectionManagerAuth } from '@votingworks/utils'; -import { Usb, readBallotPackageFromUsb } from '@votingworks/backend'; +import { + createUiStringsApi, + Usb, + readBallotPackageFromUsb, +} from '@votingworks/backend'; import { Logger } from '@votingworks/logging'; import { electionGeneralDefinition } from '@votingworks/fixtures'; import { useDevDockRouter } from '@votingworks/dev-dock-backend'; @@ -37,7 +41,8 @@ function constructAuthMachineState( }; } -function buildApi( +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function buildApi( auth: InsertedSmartCardAuthApi, usb: Usb, logger: Logger, @@ -139,6 +144,11 @@ function buildApi( return ok(electionDefinition); }, + + ...createUiStringsApi({ + logger, + store: workspace.store.getUiStringsStore(), + }), }); } diff --git a/apps/mark/backend/src/app.ui_strings.test.ts b/apps/mark/backend/src/app.ui_strings.test.ts new file mode 100644 index 0000000000..45b6498851 --- /dev/null +++ b/apps/mark/backend/src/app.ui_strings.test.ts @@ -0,0 +1,22 @@ +import tmp from 'tmp'; + +import { createMockUsb, runUiStringApiTests } from '@votingworks/backend'; +import { fakeLogger } from '@votingworks/logging'; + +import { buildMockInsertedSmartCardAuth } from '@votingworks/auth'; +import { Store } from './store'; +import { createWorkspace } from './util/workspace'; +import { buildApi } from './app'; + +const store = Store.memoryStore(); +const workspace = createWorkspace(tmp.dirSync().name, { store }); + +runUiStringApiTests({ + api: buildApi( + buildMockInsertedSmartCardAuth(), + createMockUsb().mock, + fakeLogger(), + workspace + ), + store: store.getUiStringsStore(), +}); diff --git a/apps/mark/backend/src/store.ts b/apps/mark/backend/src/store.ts index 4fbe5f00de..298fb4956d 100644 --- a/apps/mark/backend/src/store.ts +++ b/apps/mark/backend/src/store.ts @@ -2,6 +2,7 @@ // The durable datastore for configuration info. // +import { UiStringsStore, createUiStringStore } from '@votingworks/backend'; import { Client as DbClient } from '@votingworks/db'; import { ElectionDefinition, @@ -17,7 +18,10 @@ const SchemaPath = join(__dirname, '../schema.sql'); * Manages a data store for imported election definition and system settings */ export class Store { - private constructor(private readonly client: DbClient) {} + private constructor( + private readonly client: DbClient, + private readonly uiStringsStore: UiStringsStore + ) {} getDbPath(): string { return this.client.getDatabasePath(); @@ -27,14 +31,18 @@ export class Store { * Builds and returns a new store whose data is kept in memory. */ static memoryStore(): Store { - return new Store(DbClient.memoryClient(SchemaPath)); + const client = DbClient.memoryClient(SchemaPath); + const uiStringsStore = createUiStringStore(client); + return new Store(client, uiStringsStore); } /** * Builds and returns a new store at `dbPath`. */ static fileStore(dbPath: string): Store { - return new Store(DbClient.fileClient(dbPath, SchemaPath)); + const client = DbClient.fileClient(dbPath, SchemaPath); + const uiStringsStore = createUiStringStore(client); + return new Store(client, uiStringsStore); } /** @@ -139,4 +147,8 @@ export class Store { if (!result) return undefined; return safeParseSystemSettings(result.data).unsafeUnwrap(); } + + getUiStringsStore(): UiStringsStore { + return this.uiStringsStore; + } } diff --git a/apps/mark/backend/src/util/workspace.ts b/apps/mark/backend/src/util/workspace.ts index 90adf379ab..c3d885f9a5 100644 --- a/apps/mark/backend/src/util/workspace.ts +++ b/apps/mark/backend/src/util/workspace.ts @@ -20,12 +20,15 @@ export interface Workspace { reset(): void; } -export function createWorkspace(root: string): Workspace { +export function createWorkspace( + root: string, + options: { store?: Store } = {} +): Workspace { const resolvedRoot = resolve(root); ensureDirSync(resolvedRoot); const dbPath = join(resolvedRoot, 'mark.db'); - const store = Store.fileStore(dbPath); + const store = options.store || Store.fileStore(dbPath); return { path: resolvedRoot, diff --git a/apps/scan/backend/schema.sql b/apps/scan/backend/schema.sql index 9a25064cbe..164e1fdde7 100644 --- a/apps/scan/backend/schema.sql +++ b/apps/scan/backend/schema.sql @@ -87,3 +87,27 @@ create unique index idx_cvr_hashes ON cvr_hashes ( cvr_id_level_2_prefix, cvr_id ); + +create table languages ( + code text primary key +); + +create table ui_strings ( + language_code text primary key, + data text not null, -- JSON blob - see libs/types/UiStringTranslationsSchema + foreign key (language_code) references languages(code) +); + +create table audio_clips ( + id text not null, + language_code text not null, + data_base64 text not null, -- Base64-encoded audio bytes + primary key (language_code, id), + foreign key (language_code) references languages(code) +); + +create table ui_string_audio_keys ( + language_code text primary key, + data text not null, -- JSON blob - see libs/types/UiStringAudioKeysSchema + foreign key (language_code) references languages(code) +); diff --git a/apps/scan/backend/src/app.ts b/apps/scan/backend/src/app.ts index 380c4441ea..171673ba41 100644 --- a/apps/scan/backend/src/app.ts +++ b/apps/scan/backend/src/app.ts @@ -17,6 +17,7 @@ import { } from '@votingworks/utils'; import express, { Application } from 'express'; import { + createUiStringsApi, ExportDataError, exportCastVoteRecordReportToUsbDrive, ExportCastVoteRecordReportToUsbDriveError, @@ -55,7 +56,8 @@ function constructAuthMachineState( }; } -function buildApi( +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function buildApi( auth: InsertedSmartCardAuthApi, machine: PrecinctScannerStateMachine, workspace: Workspace, @@ -367,6 +369,11 @@ function buildApi( supportsUltrasonic(): boolean { return machine.supportsUltrasonic(); }, + + ...createUiStringsApi({ + logger, + store: workspace.store.getUiStringsStore(), + }), }); } diff --git a/apps/scan/backend/src/app_ui_strings.test.ts b/apps/scan/backend/src/app_ui_strings.test.ts new file mode 100644 index 0000000000..b52678b689 --- /dev/null +++ b/apps/scan/backend/src/app_ui_strings.test.ts @@ -0,0 +1,35 @@ +import tmp from 'tmp'; + +import { runUiStringApiTests } from '@votingworks/backend'; +import { buildMockInsertedSmartCardAuth } from '@votingworks/auth'; +import { fakeLogger } from '@votingworks/logging'; +import { createMockUsbDrive } from '@votingworks/usb-drive'; + +import { Store } from './store'; +import { buildApi } from './app'; +import { createWorkspace } from './util/workspace'; + +const store = Store.memoryStore(); +const workspace = createWorkspace(tmp.dirSync().name, { store }); +const mockUsbDrive = createMockUsbDrive(); + +afterAll(() => { + workspace.reset(); +}); + +runUiStringApiTests({ + api: buildApi( + buildMockInsertedSmartCardAuth(), + { + accept: jest.fn(), + return: jest.fn(), + scan: jest.fn(), + status: jest.fn(), + supportsUltrasonic: jest.fn(), + }, + workspace, + mockUsbDrive.usbDrive, + fakeLogger() + ), + store: store.getUiStringsStore(), +}); diff --git a/apps/scan/backend/src/store.ts b/apps/scan/backend/src/store.ts index 122cd299cb..05ebd81003 100644 --- a/apps/scan/backend/src/store.ts +++ b/apps/scan/backend/src/store.ts @@ -32,7 +32,11 @@ import { sha256 } from 'js-sha256'; import { DateTime } from 'luxon'; import { join } from 'path'; import { v4 as uuid } from 'uuid'; -import { ResultSheet } from '@votingworks/backend'; +import { + ResultSheet, + UiStringsStore, + createUiStringStore, +} from '@votingworks/backend'; import { clearCastVoteRecordHashes, getCastVoteRecordRootHash, @@ -101,7 +105,10 @@ function resultSheetRowToResultSheet(row: ResultSheetRow): ResultSheet { * interpreted by reading the sheets. */ export class Store { - private constructor(private readonly client: DbClient) {} + private constructor( + private readonly client: DbClient, + private readonly uiStringsStore: UiStringsStore + ) {} getDbPath(): string { return this.client.getDatabasePath(); @@ -119,14 +126,18 @@ export class Store { * Builds and returns a new store whose data is kept in memory. */ static memoryStore(): Store { - return new Store(DbClient.memoryClient(SchemaPath)); + const client = DbClient.memoryClient(SchemaPath); + const uiStringsStore = createUiStringStore(client); + return new Store(client, uiStringsStore); } /** * Builds and returns a new store at `dbPath`. */ static fileStore(dbPath: string): Store { - return new Store(DbClient.fileClient(dbPath, SchemaPath)); + const client = DbClient.fileClient(dbPath, SchemaPath); + const uiStringsStore = createUiStringStore(client); + return new Store(client, uiStringsStore); } // TODO(jonah): Make this the only way to access the store so that we always @@ -572,7 +583,7 @@ export class Store { // Adding or deleting sheets would have updated the CVR count const { maxSheetsCreatedAt, maxSheetsDeletedAt } = this.client.one(` select - max(created_at) as maxSheetsCreatedAt, + max(created_at) as maxSheetsCreatedAt, max(deleted_at) as maxSheetsDeletedAt from sheets `) as { @@ -966,4 +977,8 @@ export class Store { clearCastVoteRecordHashes(): void { clearCastVoteRecordHashes(this.client); } + + getUiStringsStore(): UiStringsStore { + return this.uiStringsStore; + } } diff --git a/apps/scan/backend/src/util/workspace.ts b/apps/scan/backend/src/util/workspace.ts index 1fcde64034..d186acb4fc 100644 --- a/apps/scan/backend/src/util/workspace.ts +++ b/apps/scan/backend/src/util/workspace.ts @@ -45,7 +45,10 @@ export interface Workspace { clearUploads(): void; } -export function createWorkspace(root: string): Workspace { +export function createWorkspace( + root: string, + options: { store?: Store } = {} +): Workspace { const resolvedRoot = resolve(root); const ballotImagesPath = join(resolvedRoot, 'ballot-images'); const scannedImagesPath = join(ballotImagesPath, 'scanned-images'); @@ -54,7 +57,7 @@ export function createWorkspace(root: string): Workspace { ensureDirSync(scannedImagesPath); const dbPath = join(resolvedRoot, 'ballots.db'); - const store = Store.fileStore(dbPath); + const store = options.store || Store.fileStore(dbPath); return { path: resolvedRoot, diff --git a/libs/backend/package.json b/libs/backend/package.json index e7749beb00..e88a3340fd 100644 --- a/libs/backend/package.json +++ b/libs/backend/package.json @@ -36,6 +36,7 @@ "dependencies": { "@votingworks/auth": "workspace:*", "@votingworks/basics": "workspace:*", + "@votingworks/db": "workspace:*", "@votingworks/fixtures": "workspace:*", "@votingworks/logging": "workspace:*", "@votingworks/types": "workspace:*", diff --git a/libs/backend/src/index.ts b/libs/backend/src/index.ts index c1a9a451b9..2571e6216c 100644 --- a/libs/backend/src/index.ts +++ b/libs/backend/src/index.ts @@ -7,3 +7,4 @@ export * from './list_directory'; export * from './mock_usb'; export * from './scan_globals'; export * from './split'; +export * from './ui_strings'; diff --git a/libs/backend/src/ui_strings/index.ts b/libs/backend/src/ui_strings/index.ts new file mode 100644 index 0000000000..9f876fe386 --- /dev/null +++ b/libs/backend/src/ui_strings/index.ts @@ -0,0 +1,4 @@ +/* istanbul ignore file */ +export * from './ui_strings_api'; +export * from './ui_strings_api_test_runner'; +export * from './ui_strings_store'; diff --git a/libs/backend/src/ui_strings/ui_strings_api.ts b/libs/backend/src/ui_strings/ui_strings_api.ts new file mode 100644 index 0000000000..51322823cb --- /dev/null +++ b/libs/backend/src/ui_strings/ui_strings_api.ts @@ -0,0 +1,55 @@ +/* istanbul ignore file - tested via VxSuite apps. */ + +import { Optional } from '@votingworks/basics'; +import { Logger } from '@votingworks/logging'; +import { + Dictionary, + LanguageCode, + UiStringAudioKeys, + UiStringTranslations, + UiStringsApi, +} from '@votingworks/types'; + +import { UiStringsStore } from './ui_strings_store'; + +/** App context for {@link UiStringsApi} endpoints. */ +export interface UiStringsApiContext { + logger: Logger; + store: UiStringsStore; +} + +/** Creates a shareable implementation of {@link UiStringsApi}. */ +export function createUiStringsApi(context: UiStringsApiContext): UiStringsApi { + const { store } = context; + + return { + getAvailableLanguages(): LanguageCode[] { + return store.getLanguages(); + }, + + getUiStrings(input: { + languageCode: LanguageCode; + }): Optional { + throw new Error( + `Not yet implemented. Requested language code: ${input.languageCode}` + ); + }, + + getUiStringAudioKeys(input: { + languageCode: LanguageCode; + }): Optional { + throw new Error( + `Not yet implemented. Requested language code: ${input.languageCode}` + ); + }, + + getAudioClipsBase64(input: { + languageCode: LanguageCode; + audioKeys: string[]; + }): Dictionary { + throw new Error( + `Not yet implemented. Requested language code: ${input.languageCode} | audioKeys: ${input.audioKeys}` + ); + }, + }; +} diff --git a/libs/backend/src/ui_strings/ui_strings_api_test_runner.ts b/libs/backend/src/ui_strings/ui_strings_api_test_runner.ts new file mode 100644 index 0000000000..ee30f14bba --- /dev/null +++ b/libs/backend/src/ui_strings/ui_strings_api_test_runner.ts @@ -0,0 +1,50 @@ +/* istanbul ignore file - test util */ + +import { LanguageCode, UiStringsApi } from '@votingworks/types'; +import { UiStringsStore } from './ui_strings_store'; + +/** Shared tests for the {@link UiStringsApi} and underlying store. */ +export function runUiStringApiTests(params: { + api: UiStringsApi; + store: UiStringsStore; +}): void { + const { api, store } = params; + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('getAvailableLanguages', () => { + expect(api.getAvailableLanguages()).toEqual([]); + + store.addLanguage(LanguageCode.ENGLISH); + store.addLanguage(LanguageCode.ENGLISH); // Should be a no-op. + expect(api.getAvailableLanguages()).toEqual([LanguageCode.ENGLISH]); + + store.addLanguage(LanguageCode.CHINESE); + expect([...api.getAvailableLanguages()].sort()).toEqual( + [LanguageCode.ENGLISH, LanguageCode.CHINESE].sort() + ); + }); + + test('getUiStrings throws not-yet-implemented error', () => { + expect(() => + api.getUiStrings({ languageCode: LanguageCode.SPANISH }) + ).toThrow(/not yet implemented/i); + }); + + test('getUiStringAudioKeys throws not-yet-implemented error', () => { + expect(() => + api.getUiStringAudioKeys({ languageCode: LanguageCode.CHINESE }) + ).toThrow(/not yet implemented/i); + }); + + test('getAudioClipsBase64 throws not-yet-implemented error', () => { + expect(() => + api.getAudioClipsBase64({ + languageCode: LanguageCode.ENGLISH, + audioKeys: ['abc123', 'd1e2f3'], + }) + ).toThrow(/not yet implemented/i); + }); +} diff --git a/libs/backend/src/ui_strings/ui_strings_store.ts b/libs/backend/src/ui_strings/ui_strings_store.ts new file mode 100644 index 0000000000..f609cd0ec8 --- /dev/null +++ b/libs/backend/src/ui_strings/ui_strings_store.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file - tested via VxSuite apps. */ + +import { Client as DbClient } from '@votingworks/db'; +import { + LanguageCode, + LanguageCodeSchema, + safeParse, +} from '@votingworks/types'; + +/** Store interface for UI String API endpoints. */ +export interface UiStringsStore { + // TODO(kofi): Fill out. + addLanguage(code: LanguageCode): void; + getLanguages(): LanguageCode[]; +} + +/** Creates a shareable implementation of the {@link UiStringsStore}. */ +export function createUiStringStore(dbClient: DbClient): UiStringsStore { + return { + addLanguage(languageCode: LanguageCode): void { + dbClient.run( + 'insert or ignore into languages (code) values (?)', + languageCode + ); + }, + + getLanguages(): LanguageCode[] { + const result = dbClient.all('select code from languages') as Array<{ + code: string; + }>; + + return result.map((row) => + safeParse(LanguageCodeSchema, row.code).unsafeUnwrap() + ); + }, + }; +} diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 0812e11ca8..bf1c2c4e45 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -18,6 +18,7 @@ export * from './generic'; export * from './geometry'; export * from './image'; export * from './interpretation'; +export * from './language_code'; export * from './numeric'; export * from './polls'; export * from './precinct_scanner'; @@ -29,5 +30,6 @@ export * from './tallies'; export * from './ui_string_audio_clips'; export * from './ui_string_audio_keys'; export * from './ui_string_translations'; +export * from './ui_strings_api'; export * from './ui_theme'; export * from './voting_method'; diff --git a/libs/types/src/language_code.ts b/libs/types/src/language_code.ts index f07f61134f..7820fd3502 100644 --- a/libs/types/src/language_code.ts +++ b/libs/types/src/language_code.ts @@ -1,6 +1,11 @@ +import { z } from 'zod'; + /* ISO 639-1 codes for supported VxSuite languages. */ export enum LanguageCode { CHINESE = 'zh', ENGLISH = 'en', SPANISH = 'es', } + +export const LanguageCodeSchema: z.ZodType = + z.nativeEnum(LanguageCode); diff --git a/libs/types/src/ui_strings_api.ts b/libs/types/src/ui_strings_api.ts new file mode 100644 index 0000000000..6e5fdbd09a --- /dev/null +++ b/libs/types/src/ui_strings_api.ts @@ -0,0 +1,27 @@ +import { Optional } from '@votingworks/basics'; + +import { Dictionary } from './generic'; +import { LanguageCode } from './language_code'; +import { UiStringAudioKeys } from './ui_string_audio_keys'; +import { UiStringTranslations } from './ui_string_translations'; + +export interface UiStringsApi { + getAvailableLanguages(): LanguageCode[]; + + getUiStrings(input: { + languageCode: LanguageCode; + }): Optional; + + getUiStringAudioKeys(input: { + languageCode: LanguageCode; + }): Optional; + + /** + * Returns a map of the given audio keys to corresponding audio data in + * Base64-encoded byte format. + */ + getAudioClipsBase64(input: { + languageCode: LanguageCode; + audioKeys: string[]; + }): Dictionary; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2565630f6..2b9916a477 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2958,6 +2958,9 @@ importers: '@votingworks/basics': specifier: workspace:* version: link:../basics + '@votingworks/db': + specifier: workspace:* + version: link:../db '@votingworks/fixtures': specifier: workspace:* version: link:../fixtures