style: lint
This commit is contained in:
@@ -14,11 +14,11 @@ const here = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function concept(overrides: Partial<Concept> = {}): Concept {
|
||||
return {
|
||||
definition: 'The surface of the application that shows the character.',
|
||||
id: 'stage',
|
||||
subject: 'application',
|
||||
terms: [{ 'part-of-speech': 'noun', 'status': 'preferred', 'text': 'Stage' }],
|
||||
translatable: true,
|
||||
definition: 'The surface of the application that shows the character.',
|
||||
terms: [{ 'text': 'Stage', 'part-of-speech': 'noun', 'status': 'preferred' }],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
@@ -83,8 +83,8 @@ describe('buildTbx', () => {
|
||||
it('writes the context of each term that has one', () => {
|
||||
const xml = buildTbx([concept({
|
||||
terms: [
|
||||
{ 'text': 'Stage', 'part-of-speech': 'noun', 'status': 'preferred', 'context': 'Disable Stage Transitions' },
|
||||
{ 'text': 'Scene', 'part-of-speech': 'noun', 'status': 'deprecated', 'type': 'variant' },
|
||||
{ 'context': 'Disable Stage Transitions', 'part-of-speech': 'noun', 'status': 'preferred', 'text': 'Stage' },
|
||||
{ 'part-of-speech': 'noun', 'status': 'deprecated', 'text': 'Scene', 'type': 'variant' },
|
||||
],
|
||||
})], 'test')
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('buildTbx', () => {
|
||||
|
||||
it('puts the context and the note after the termNote elements', () => {
|
||||
const xml = buildTbx([concept({
|
||||
terms: [{ 'text': 'Stage', 'part-of-speech': 'noun', 'status': 'preferred', 'context': 'A sentence', 'note': 'A note' }],
|
||||
terms: [{ 'context': 'A sentence', 'note': 'A note', 'part-of-speech': 'noun', 'status': 'preferred', 'text': 'Stage' }],
|
||||
})], 'test')
|
||||
|
||||
expect(xml.indexOf('administrativeStatus')).toBeLessThan(xml.indexOf('type="context"'))
|
||||
@@ -106,9 +106,9 @@ describe('buildTbx', () => {
|
||||
it('maps each status to the suffixed value that Crowdin writes', () => {
|
||||
const xml = buildTbx([concept({
|
||||
terms: [
|
||||
{ 'text': 'Character Card', 'part-of-speech': 'noun', 'status': 'preferred' },
|
||||
{ 'text': 'Card', 'part-of-speech': 'noun', 'status': 'admitted', 'type': 'shortForm' },
|
||||
{ 'text': 'Deck', 'part-of-speech': 'noun', 'status': 'deprecated', 'gender': 'feminine' },
|
||||
{ 'part-of-speech': 'noun', 'status': 'preferred', 'text': 'Character Card' },
|
||||
{ 'part-of-speech': 'noun', 'status': 'admitted', 'text': 'Card', 'type': 'shortForm' },
|
||||
{ 'gender': 'feminine', 'part-of-speech': 'noun', 'status': 'deprecated', 'text': 'Deck' },
|
||||
],
|
||||
})], 'test')
|
||||
|
||||
@@ -146,8 +146,8 @@ describe('parseGlossary', () => {
|
||||
// status inside one concept, so nothing here may forbid it.
|
||||
const twoPreferred = concept({
|
||||
terms: [
|
||||
{ 'text': 'AIRI', 'part-of-speech': 'proper noun', 'status': 'preferred' },
|
||||
{ 'text': 'Project AIRI', 'part-of-speech': 'proper noun', 'status': 'preferred' },
|
||||
{ 'part-of-speech': 'proper noun', 'status': 'preferred', 'text': 'AIRI' },
|
||||
{ 'part-of-speech': 'proper noun', 'status': 'preferred', 'text': 'Project AIRI' },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -161,10 +161,10 @@ describe('parseGlossary', () => {
|
||||
// says, and rejected "proper noun" with a space. A glossary exported from project 816610
|
||||
// writes the spaced form on every proper noun, so the requirement was backwards: it
|
||||
// would have rejected the data Crowdin itself produces.
|
||||
const spaced = concept({ terms: [{ 'text': 'AIRI', 'part-of-speech': 'proper noun', 'status': 'preferred' }] })
|
||||
const spaced = concept({ terms: [{ 'part-of-speech': 'proper noun', 'status': 'preferred', 'text': 'AIRI' }] })
|
||||
|
||||
expect(parseGlossary([spaced])).toHaveLength(1)
|
||||
expect(() => parseGlossary([concept({ terms: [{ 'text': 'AIRI', 'part-of-speech': 'properNoun', 'status': 'preferred' }] })])).toThrow()
|
||||
expect(() => parseGlossary([concept({ terms: [{ 'part-of-speech': 'properNoun', 'status': 'preferred', 'text': 'AIRI' }] })])).toThrow()
|
||||
})
|
||||
|
||||
it('rejects an id that is not kebab case', () => {
|
||||
@@ -276,7 +276,7 @@ describe('terms.yaml', () => {
|
||||
// A translated token breaks stage playback with no error message, so a token must never
|
||||
// reach the glossary and become a term a translator is invited to work on.
|
||||
const code = concepts.flatMap(c => c.terms
|
||||
.filter(t => ['ACT', 'DELAY', 'CALL'].includes(t.text) || /[/{}<>]/.test(t.text))
|
||||
.filter(t => ['ACT', 'CALL', 'DELAY'].includes(t.text) || /[/{}<>]/.test(t.text))
|
||||
.map(t => t.text))
|
||||
|
||||
expect(code).toEqual([])
|
||||
|
||||
@@ -45,30 +45,37 @@ const XML_MODELS = [
|
||||
* them.
|
||||
*/
|
||||
const ADMINISTRATIVE_STATUS: Record<Term['status'], string> = {
|
||||
preferred: 'preferredTerm-admn-sts',
|
||||
admitted: 'admittedTerm-admn-sts',
|
||||
deprecated: 'deprecatedTerm-admn-sts',
|
||||
preferred: 'preferredTerm-admn-sts',
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the text of a YAML folded block into paragraphs.
|
||||
* Renders the glossary as a TBX-Basic document.
|
||||
*
|
||||
* A folded block (`>`) joins each wrapped line with a space and turns a blank line into one
|
||||
* newline character. So a newline that survives parsing is always a paragraph break, and
|
||||
* the line width of the source file never reaches the output.
|
||||
* The result is a complete XML document with the declaration, so a caller writes it to disk
|
||||
* unchanged. It carries the English terms only; Crowdin owns every other language, and
|
||||
* uploading target terms would overwrite the work of a translator.
|
||||
*
|
||||
* NOTICE:
|
||||
* Splitting on two or more newlines looks correct and is wrong. YAML gives a blank line a
|
||||
* single newline, so that pattern matches nothing and every paragraph merges into one.
|
||||
*
|
||||
* Before:
|
||||
* - "One paragraph.\nA second paragraph."
|
||||
*
|
||||
* After:
|
||||
* - "One paragraph.\n\nA second paragraph."
|
||||
* @param concepts Validated concepts, in the order they appear in `terms.yaml`.
|
||||
* @param source A description of where the data came from, recorded in the TBX header.
|
||||
*/
|
||||
function collapse(text: string): string {
|
||||
return text.split(/\n+/).map(paragraph => paragraph.replace(/\s+/g, ' ').trim()).filter(Boolean).join('\n\n')
|
||||
export function buildTbx(concepts: Concept[], source: string): string {
|
||||
const tree = x(
|
||||
'tbx',
|
||||
// The root carries all four attributes that TBX-Basic requires. `style="dca"` selects
|
||||
// the data-category-as-attribute style, which is the one the schema values assume.
|
||||
{ 'style': 'dca', 'type': 'TBX-Basic', 'xml:lang': SOURCE_LANGUAGE, 'xmlns': TBX_NAMESPACE },
|
||||
[
|
||||
// The header carries a sourceDesc and nothing else, which is the shape Crowdin writes.
|
||||
x('tbxHeader', [x('fileDesc', [x('sourceDesc', [x('p', source)])])]),
|
||||
x('text', [x('body', concepts.map(buildConceptEntry))]),
|
||||
],
|
||||
)
|
||||
|
||||
indent(tree, 0)
|
||||
|
||||
return ['<?xml version="1.0" encoding="UTF-8"?>', ...XML_MODELS, toXml(tree), ''].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +101,7 @@ function buildConceptEntry(concept: Concept): Element {
|
||||
conceptChildren.push(x('note', collapse(concept.note)))
|
||||
|
||||
if (concept.url)
|
||||
conceptChildren.push(x('xref', { type: 'externalCrossReference', target: concept.url }, concept.url))
|
||||
conceptChildren.push(x('xref', { target: concept.url, type: 'externalCrossReference' }, concept.url))
|
||||
|
||||
const termSections = concept.terms.map((term) => {
|
||||
const termChildren: Element[] = [
|
||||
@@ -125,31 +132,24 @@ function buildConceptEntry(concept: Concept): Element {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the glossary as a TBX-Basic document.
|
||||
* Turns the text of a YAML folded block into paragraphs.
|
||||
*
|
||||
* The result is a complete XML document with the declaration, so a caller writes it to disk
|
||||
* unchanged. It carries the English terms only; Crowdin owns every other language, and
|
||||
* uploading target terms would overwrite the work of a translator.
|
||||
* A folded block (`>`) joins each wrapped line with a space and turns a blank line into one
|
||||
* newline character. So a newline that survives parsing is always a paragraph break, and
|
||||
* the line width of the source file never reaches the output.
|
||||
*
|
||||
* @param concepts Validated concepts, in the order they appear in `terms.yaml`.
|
||||
* @param source A description of where the data came from, recorded in the TBX header.
|
||||
* NOTICE:
|
||||
* Splitting on two or more newlines looks correct and is wrong. YAML gives a blank line a
|
||||
* single newline, so that pattern matches nothing and every paragraph merges into one.
|
||||
*
|
||||
* Before:
|
||||
* - "One paragraph.\nA second paragraph."
|
||||
*
|
||||
* After:
|
||||
* - "One paragraph.\n\nA second paragraph."
|
||||
*/
|
||||
export function buildTbx(concepts: Concept[], source: string): string {
|
||||
const tree = x(
|
||||
'tbx',
|
||||
// The root carries all four attributes that TBX-Basic requires. `style="dca"` selects
|
||||
// the data-category-as-attribute style, which is the one the schema values assume.
|
||||
{ 'type': 'TBX-Basic', 'style': 'dca', 'xml:lang': SOURCE_LANGUAGE, 'xmlns': TBX_NAMESPACE },
|
||||
[
|
||||
// The header carries a sourceDesc and nothing else, which is the shape Crowdin writes.
|
||||
x('tbxHeader', [x('fileDesc', [x('sourceDesc', [x('p', source)])])]),
|
||||
x('text', [x('body', concepts.map(buildConceptEntry))]),
|
||||
],
|
||||
)
|
||||
|
||||
indent(tree, 0)
|
||||
|
||||
return ['<?xml version="1.0" encoding="UTF-8"?>', ...XML_MODELS, toXml(tree), ''].join('\n')
|
||||
function collapse(text: string): string {
|
||||
return text.split(/\n+/).map(paragraph => paragraph.replace(/\s+/g, ' ').trim()).filter(Boolean).join('\n\n')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,14 +56,14 @@ const GenderSchema = v.picklist(['masculine', 'feminine', 'neuter', 'other'])
|
||||
* note -> <note>
|
||||
*/
|
||||
const TermSchema = v.object({
|
||||
'text': v.pipe(v.string(), v.nonEmpty()),
|
||||
'part-of-speech': PartOfSpeechSchema,
|
||||
'status': UsageStatusSchema,
|
||||
'type': v.optional(TermTypeSchema),
|
||||
'gender': v.optional(GenderSchema),
|
||||
/** A sentence that shows the term in use. TBX allows a context at term level only. */
|
||||
'context': v.optional(v.string()),
|
||||
'gender': v.optional(GenderSchema),
|
||||
'note': v.optional(v.string()),
|
||||
'part-of-speech': PartOfSpeechSchema,
|
||||
'status': UsageStatusSchema,
|
||||
'text': v.pipe(v.string(), v.nonEmpty()),
|
||||
'type': v.optional(TermTypeSchema),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -80,18 +80,18 @@ const TermSchema = v.object({
|
||||
* terms -> <langSec xml:lang="en"> with one <termSec> for each term
|
||||
*/
|
||||
const ConceptSchema = v.object({
|
||||
/** One sentence. Write two only when one cannot carry the meaning. */
|
||||
definition: v.pipe(v.string(), v.nonEmpty()),
|
||||
/**
|
||||
* Stable identifier in kebab case. A change here creates a new concept in Crowdin rather
|
||||
* than updating the existing one, so it must survive a rename of the terms.
|
||||
*/
|
||||
id: v.pipe(v.string(), v.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'id must be kebab case')),
|
||||
subject: v.pipe(v.string(), v.nonEmpty()),
|
||||
translatable: v.boolean(),
|
||||
/** One sentence. Write two only when one cannot carry the meaning. */
|
||||
definition: v.pipe(v.string(), v.nonEmpty()),
|
||||
note: v.optional(v.string()),
|
||||
url: v.optional(v.pipe(v.string(), v.url())),
|
||||
subject: v.pipe(v.string(), v.nonEmpty()),
|
||||
terms: v.pipe(v.array(TermSchema), v.minLength(1)),
|
||||
translatable: v.boolean(),
|
||||
url: v.optional(v.pipe(v.string(), v.url())),
|
||||
})
|
||||
|
||||
export const GlossarySchema = v.pipe(
|
||||
@@ -103,8 +103,8 @@ export const GlossarySchema = v.pipe(
|
||||
),
|
||||
)
|
||||
|
||||
export type Term = v.InferOutput<typeof TermSchema>
|
||||
export type Concept = v.InferOutput<typeof ConceptSchema>
|
||||
export type Term = v.InferOutput<typeof TermSchema>
|
||||
|
||||
/**
|
||||
* Parses and validates the contents of `terms.yaml`.
|
||||
|
||||
Reference in New Issue
Block a user