Skip to content
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ export const recommendedTest_6_2_27: DocumentTest
export const recommendedTest_6_2_28: DocumentTest
export const recommendedTest_6_2_29: DocumentTest
export const recommendedTest_6_2_30: DocumentTest
export const recommendedTest_6_2_39_2: DocumentTest
export const recommendedTest_6_2_43: DocumentTest
```

Expand Down
1 change: 1 addition & 0 deletions csaf_2_1/recommendedTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_2
export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js'
export { recommendedTest_6_2_30 } from './recommendedTests/recommendedTest_6_2_30.js'
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
export { recommendedTest_6_2_39_2 } from './recommendedTests/recommendedTest_6_2_39_2.js'
export { recommendedTest_6_2_43 } from './recommendedTests/recommendedTest_6_2_43.js'
107 changes: 107 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Ajv from 'ajv/dist/jtd.js'
import {
containsOneNoteWithTitleAndCategory,
getTranslationInDocumentLang,
isLangSpecifiedAndNotEnglish,
} from '../../lib/shared/languageSpecificTranslation.js'

const ajv = new Ajv()

/*
This is the jtd schema that needs to match the input document so that the
test is activated. If this schema doesn't match it normally means that the input
document does not validate against the csaf json schema or optional fields that
the test checks are not present.
*/
const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
document: {
additionalProperties: true,
properties: {
category: { type: 'string' },
},
optionalProperties: {
lang: {
type: 'string',
},
notes: {
elements: {
additionalProperties: true,
optionalProperties: {
category: {
type: 'string',
},
title: {
type: 'string',
},
},
},
},
},
},
},
})

const validateSchema = ajv.compile(inputSchema)

/**
* If the document language is specified but not English, it MUST be tested that exactly one item in document
* notes exists that has the language specific translation of the term Reasoning for Withdrawal as title.
* The category of this item MUST be description. If no language-specific translation has been recorded,
* the test MUST be skipped and output an information to the user that no such translation is known.
*
* @param {unknown} doc
*/
export function recommendedTest_6_2_39_2(doc) {
/*
The `ctx` variable holds the state that is accumulated during the test run and is
finally returned by the function.
*/
/** @type { {warnings: Array<{ message: string; instancePath: string }>;
* infos: Array<{ message: string; instancePath: string }>}} */
const ctx = {
warnings: [],
infos: [],
}

const noteCategory = 'description'

if (!validateSchema(doc) || doc.document.category !== 'csaf_withdrawn') {
return ctx
}

const withdrawalInDocLang = getTranslationInDocumentLang(
doc,
'reasoning_for_withdrawal'
)
if (!withdrawalInDocLang) {
ctx.infos.push({
instancePath: '/document/notes',
message:
'no language specific translation for "Reasoning for Withdrawal" has been recorded',
})
return ctx
}

if (isLangSpecifiedAndNotEnglish(doc.document.lang)) {
const notes = doc.document.notes
if (
!notes ||
!containsOneNoteWithTitleAndCategory(
notes,
withdrawalInDocLang,
noteCategory
)
) {
ctx.warnings.push({
instancePath: '/document/notes',
message:
`for document category "csaf_withdrawn" exactly one note must exist ` +
`with note category "${noteCategory}" and title "${withdrawalInDocLang}`,
})
}
}

return ctx
}
17 changes: 17 additions & 0 deletions lib/language_specific_translation/translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* JavaScript version of JSON file: csaf_2.1/language_specific_translation/translations.json
*/
export default {
$schema:
'https://raw.githubusercontent.com/oasis-tcs/csaf/master/csaf_2.1/test/language_specific_translation/translations_json_schema.json',
translation_version: '2.1',
translation: {
de: {
license: 'Lizenz',
product_description: 'Produktbeschreibung',
reasoning_for_supersession: 'Begründung für die Ersetzung',
reasoning_for_withdrawal: 'Begründung für die Zurückziehung',
superseding_document: 'Ersetzendes Dokument',
},
},
}
79 changes: 79 additions & 0 deletions lib/shared/languageSpecificTranslation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import bcp47 from 'bcp47'
import translations from '../../lib/language_specific_translation/translations.js'

const csafTranslationMaps = new Map(
Object.entries(translations.translation).map(([key, value]) => [
key,
new Map(Object.entries(value)),
])
)

/**
* Checks if the document language is specified and not English
*
* @param {string | undefined} language - The language expression to check
* @returns {boolean} False if language is English, true if the language is valid, false otherwise
*/
export function isLangSpecifiedAndNotEnglish(language) {
return (
!!language && !(bcp47.parse(language)?.langtag.language.language === 'en')
)
}

/**
* test whether exactly one item in document notes exists that has the given title.
* and the given category.
* @param {Array<{ category?: string | undefined; title?: string | undefined; }>} notes
* @param {string} titleToFind
* @param {string} category
* @returns {boolean} True if the language is valid, false otherwise
*/
export function containsOneNoteWithTitleAndCategory(
notes,
titleToFind,
category
) {
return (
notes.filter(
(note) => note.category === category && note.title === titleToFind
).length === 1
)
}

/**
* Get the language specific translation of the given i18nKey
* @param {{ document: { lang?: string; }; }} doc
* @param {string} i18nKey
*/
export function getTranslationInDocumentLang(doc, i18nKey) {
return doc.document.lang
? getTranslationInMap(doc.document.lang, i18nKey, csafTranslationMaps)
: undefined
}

/**
* Get the language specific translation of the given i18nKey in translationMaps
* @param {string} langToTranslate
* @param {string} i18nKey
* @param {Map<string,Map<string,string>>} translationMaps
*/
export function getTranslationInMap(langToTranslate, i18nKey, translationMaps) {
const langtag = bcp47.parse(langToTranslate)?.langtag
const languageCode = langtag?.language.language
let transMapForLanguage
if (langtag && languageCode) {
if (langtag.region) {
if (langtag.script) {
transMapForLanguage = translationMaps.get(
`${languageCode}-${langtag.script}-${langtag.region}`
)
}
transMapForLanguage =
transMapForLanguage ??
translationMaps.get(`${languageCode}-${langtag.region}`)
}
transMapForLanguage =
transMapForLanguage ?? translationMaps.get(languageCode)
}
return transMapForLanguage?.get(i18nKey)
}
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ const excluded = [
'6.2.36',
'6.2.37',
'6.2.39.1',
'6.2.39.2',
'6.2.39.3',
'6.2.39.4',
'6.2.40',
Expand Down
49 changes: 49 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_39_2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { recommendedTest_6_2_39_2 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js'
import { expect } from 'chai'
import assert from 'node:assert'
import { getTranslationInDocumentLang } from '../../lib/shared/languageSpecificTranslation.js'

describe('recommendedTest_6_2_39_2', function () {
it('only runs on relevant documents', function () {
assert.equal(recommendedTest_6_2_39_2({}).warnings.length, 0)
})

it('only runs on valid category', function () {
const result = recommendedTest_6_2_39_2({
document: { category: '123', license_expression: 'MIT' },
})

assert.equal(result.warnings.length, 0)
assert.equal(result.infos.length, 0)
})

it('info on invalid language', function () {
const result = recommendedTest_6_2_39_2({
document: {
category: 'csaf_withdrawn',
lang: '123',
license_expression: 'MIT',
},
})
assert.equal(result.warnings.length, 0)
assert.equal(result.infos.length, 1)
})

it('check get ReasoningForWithdrawal in document lang', function () {
expect(
getTranslationInDocumentLang(
{ document: { lang: 'de' } },
'reasoning_for_withdrawal'
)
).to.eq('Begründung für die Zurückziehung')
expect(
getTranslationInDocumentLang(
{ document: { lang: 'es' } },
'reasoning_for_withdrawal'
)
).to.eq(undefined)
expect(
getTranslationInDocumentLang({ document: {} }, 'reasoning_for_withdrawal')
).to.eq(undefined)
})
})
39 changes: 39 additions & 0 deletions tests/languageSpecificTranslation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getTranslationInMap } from '../lib/shared/languageSpecificTranslation.js'
import { expect } from 'chai'

describe('test language specific translation', function () {
it('test getTranslationInMap', function () {
const translationMaps = new Map([
['de', new Map([['I18nTestKey', 'translationDe']])],
['de-AT', new Map([['I18nTestKey', 'translationAT']])],
['en', new Map([['I18nTestKey', 'translationEn']])],
['zh-Hans-CN', new Map([['I18nTestKey', 'translationZh-Hans-CN']])],
['sr', new Map([['I18nTestKey', 'translationSr']])],
])

expect(
getTranslationInMap('de', 'I18nTestKey', translationMaps),
'Translate language code de'
).to.equal('translationDe')
expect(
getTranslationInMap('de-AT', 'I18nTestKey', translationMaps),
'Translate language and region code'
).to.equal('translationAT')
expect(
getTranslationInMap('en', 'I18nTestKey', translationMaps),
'Translate language code en'
).to.equal('translationEn')
expect(
getTranslationInMap('en-US', 'I18nTestKey', translationMaps),
'Fallback to language code en on region us'
).to.equal('translationEn')
expect(
getTranslationInMap('zh-Hans-CN', 'I18nTestKey', translationMaps),
'Translate language, region and script code'
).to.equal('translationZh-Hans-CN')
expect(
getTranslationInMap('sr-Cyrl-RS', 'I18nTestKey', translationMaps),
'Fallback to language code en on region an sript code'
).to.equal('translationSr')
})
})