style: lint

This commit is contained in:
Neko Ayaka
2026-08-26 19:49:58 +08:00
parent e60a04a4ec
commit 98f40d7d0b
1625 changed files with 75216 additions and 75203 deletions
+13 -13
View File
@@ -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([])
+39 -39
View File
@@ -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')
}
/**
+11 -11
View File
@@ -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`.