-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* initial implementation of `TranslationApi` * * add @types/json-stable-strinfify * start writing unit tests * error handling for missing translation * fix error in sql query * * remove `message` from object when hashing * fix logic error when `put` * handle unexisting key in the indexing (by creating a new Set) * make `fieldRef` and `regionCode` optional when `get` * improve `get()` by ignoring not translated languages, improve typing * * make `index` private, run it on `put` * expose cache map through symbol and getter * improve `get` signature (by making `fieldRef` and `regionCode` optional) * add more unit tests * revert `index` as private method, update tests * update magic-bytes manually * add e2e/translation-api.js and expose translation api in mapeo project * * add translationTable to index writer * add translationDoc to entries for batching indexer * move decoding of translationDoc into `.index` function in translationApi * rever changes to `.index` method (better to accept doc than block) * first e2e tests * revert regionCode fallback (since its handled in an upper layer) * use default config for translationApi e2e tests, test with a bunch of translations * add translation fixtures * add check of expected translation * improve test messages * add assertion of matching preset docId with translation docIdRef * add tests and fixture for fields * chore: use private members for TranslationApi (#579) * chore: ensure translation tests are checking something (#578) These tests iterate over various documents to check things. If, somehow, we had 0 documents, the test would pass incorrectly. This fixes that by checking that we have at least one test doc. * Apply suggestions from code review 1. replace conditional with assertion 2. remove comment in `get` signature 3. remove `?` from `translatedSchemas.add` 4. Add comment to advice only using private symbol in tests Co-authored-by: Evan Hahn <[email protected]> * Fix `assert` not being present * perf test, return full doc from create * fix tests after api change * comment perf test and re indexing of translations on translationAPI constructor * added cache tests * simplify `put` logic * doc can be a const inside try now * add return type to `get` Co-authored-by: Evan Hahn <[email protected]> * chore: add "ready" promise to translation API (#589) * chore: use `instanceof`, not message, for "not found" error (#590) --------- Co-authored-by: Tomás Ciccola <[email protected]> Co-authored-by: Evan Hahn <[email protected]>
- Loading branch information
1 parent
782cfe5
commit 9fa05aa
Showing
11 changed files
with
779 additions
and
207 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// @ts-check | ||
|
||
export class NotFoundError extends Error { | ||
constructor() { | ||
super('Not found') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { and, eq } from 'drizzle-orm' | ||
import { kCreateWithDocId, kSelect } from './datatype/index.js' | ||
import { hashObject } from './utils.js' | ||
import { NotFoundError } from './errors.js' | ||
|
||
export const ktranslatedLanguageCodeToSchemaNames = Symbol( | ||
'translatedLanguageCodeToSchemaNames' | ||
) | ||
export default class TranslationApi { | ||
/** @type {Map< | ||
* import('@mapeo/schema').TranslationValue['languageCode'], | ||
* Set<import('@mapeo/schema/dist/types.js').SchemaName>>} */ | ||
#translatedLanguageCodeToSchemaNames = new Map() | ||
#dataType | ||
#table | ||
#indexPromise | ||
|
||
/** | ||
* @param {Object} opts | ||
* @param {import('./datatype/index.js').DataType< | ||
* import('./datastore/index.js').DataStore<'config'>, | ||
* typeof import('./schema/project.js').translationTable, | ||
* 'translation', | ||
* import('@mapeo/schema').Translation, | ||
* import('@mapeo/schema').TranslationValue | ||
* >} opts.dataType | ||
* @param {typeof import('./schema/project.js').translationTable} opts.table | ||
*/ | ||
constructor({ dataType, table }) { | ||
this.#dataType = dataType | ||
this.#table = table | ||
this.#indexPromise = this.#dataType | ||
.getMany() | ||
.then((docs) => { | ||
docs.map((doc) => this.index(doc)) | ||
}) | ||
.catch((err) => { | ||
throw new Error(`error loading Translation cache: ${err}`) | ||
}) | ||
} | ||
|
||
/** @returns {Promise<void>} */ | ||
ready() { | ||
return this.#indexPromise | ||
} | ||
|
||
/** | ||
* @param {import('@mapeo/schema').TranslationValue} value | ||
*/ | ||
async put(value) { | ||
/* eslint-disable no-unused-vars */ | ||
const { message, ...identifiers } = value | ||
const docId = hashObject(identifiers) | ||
try { | ||
const doc = await this.#dataType.getByDocId(docId) | ||
return await this.#dataType.update(doc.versionId, value) | ||
} catch (e) { | ||
if (e instanceof NotFoundError) { | ||
return await this.#dataType[kCreateWithDocId](docId, value) | ||
} else { | ||
throw new Error(`Error on translation ${e}`) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @param {import('type-fest').SetOptional< | ||
* Omit<import('@mapeo/schema').TranslationValue,'schemaName' | 'message'>, | ||
* 'fieldRef' | 'regionCode'>} value | ||
* @returns {Promise<import('@mapeo/schema').Translation[]>} | ||
*/ | ||
async get(value) { | ||
await this.ready() | ||
|
||
const docTypeIsTranslatedToLanguage = | ||
this.#translatedLanguageCodeToSchemaNames | ||
.get(value.languageCode) | ||
?.has( | ||
/** @type {import('@mapeo/schema/dist/types.js').SchemaName} */ ( | ||
value.schemaNameRef | ||
) | ||
) | ||
if (!docTypeIsTranslatedToLanguage) return [] | ||
|
||
const filters = [ | ||
eq(this.#table.docIdRef, value.docIdRef), | ||
eq(this.#table.schemaNameRef, value.schemaNameRef), | ||
eq(this.#table.languageCode, value.languageCode), | ||
] | ||
if (value.fieldRef) { | ||
filters.push(eq(this.#table.fieldRef, value.fieldRef)) | ||
} | ||
|
||
if (value.regionCode) { | ||
filters.push(eq(this.#table.regionCode, value.regionCode)) | ||
} | ||
|
||
return (await this.#dataType[kSelect]()) | ||
.where(and.apply(null, filters)) | ||
.prepare() | ||
.all() | ||
} | ||
|
||
/** | ||
* @param {import('@mapeo/schema').TranslationValue} doc | ||
*/ | ||
index(doc) { | ||
let translatedSchemas = this.#translatedLanguageCodeToSchemaNames.get( | ||
doc.languageCode | ||
) | ||
if (!translatedSchemas) { | ||
translatedSchemas = new Set() | ||
this.#translatedLanguageCodeToSchemaNames.set( | ||
doc.languageCode, | ||
translatedSchemas | ||
) | ||
} | ||
translatedSchemas.add( | ||
/** @type {import('@mapeo/schema/dist/types.js').SchemaName} */ ( | ||
doc.schemaNameRef | ||
) | ||
) | ||
} | ||
|
||
// This should only be used by tests. | ||
get [ktranslatedLanguageCodeToSchemaNames]() { | ||
return this.#translatedLanguageCodeToSchemaNames | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
const commonPreset = { | ||
/** @type {'translation'} */ | ||
schemaName: 'translation', | ||
schemaNameRef: 'preset', | ||
languageCode: 'es', | ||
regionCode: 'AR', | ||
fieldRef: 'name', | ||
} | ||
|
||
const commonField = { | ||
/** @type {'translation'} */ | ||
schemaName: 'translation', | ||
schemaNameRef: 'field', | ||
languageCode: 'es', | ||
regionCode: 'AR', | ||
fieldRef: 'label', | ||
} | ||
|
||
/** @type {Object.<string,string>} */ | ||
export const presetsTranslationMap = { | ||
Airstrip: 'Pista de Aterrizaje', | ||
Boundry: 'Límite', | ||
Cave: 'Cueva', | ||
Building: 'Edificio', | ||
Clay: 'Arcilla', | ||
'New Area': 'Nueva Área', | ||
Camp: 'Campamento', | ||
Community: 'Comunidad', | ||
'Gathering Site': 'Zona de recolección', | ||
Hills: 'Colinas', | ||
House: 'Casa', | ||
'Hunting Site': 'Sitio de Caza', | ||
'Fishing Site': 'Sitio de Pesca', | ||
Palm: 'Palma', | ||
Plant: 'Planta', | ||
Path: 'Camino', | ||
'New point': 'Nuevo punto', | ||
River: 'Río', | ||
'New line': 'Nueva línea', | ||
Lake: 'Lago', | ||
Stream: 'Cauce', | ||
'Special site': 'Sitio especial', | ||
Farmland: 'Tierra de cultivo', | ||
Threat: 'Amenaza', | ||
Waterfall: 'Cascada', | ||
Tree: 'Árbol', | ||
} | ||
|
||
/** @type {Object.<string,string>} */ | ||
export const fieldsTranslationMap = { | ||
'Animal type': 'Tipo de animal', | ||
'Building type': 'Tipo de edificio', | ||
'What is gathered here?': '¿Qué se recolecta aquí?', | ||
Note: 'Nota', | ||
Owner: 'Dueño', | ||
'Plant species': 'Especie de planta', | ||
'What kind of path?': '¿Qué clase de camino?', | ||
Name: 'Nombre', | ||
'Tree species': 'Especie de árbol', | ||
} | ||
|
||
export const presetTranslations = Object.keys(presetsTranslationMap).map( | ||
(key) => { | ||
const translation = presetsTranslationMap[key] | ||
return { ...commonPreset, message: translation } | ||
} | ||
) | ||
|
||
export const fieldTranslations = Object.keys(fieldsTranslationMap).map( | ||
(key) => { | ||
const translation = fieldsTranslationMap[key] | ||
return { ...commonField, message: translation } | ||
} | ||
) |
Oops, something went wrong.