mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
ci(i18n): stop the crowdin sync failing on a single translator edit
Fail the nightly export only when a catalog loses far more than churn explains, rewrite a Crowdin em dash to the hyphen the source uses instead of discarding the translation, and report a rejected message as a rejection rather than a lost one. See docs/LOCALIZATION.md.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
// Catalogs never carry a Unicode em dash. That is a house style rule rather than a correctness one,
|
||||
// and translators reach for the character constantly, so rejecting the message would discard a sound
|
||||
// translation over punctuation and stall the sync until somebody edits Crowdin by hand. The English
|
||||
// source writes a plain hyphen in the same place, so the sync rewrites the character instead.
|
||||
export function normalizeMessageTypography(message) {
|
||||
return message.replaceAll('\u2014', '-')
|
||||
}
|
||||
|
||||
export function findTypographyDrift({ catalogs }) {
|
||||
const drift = []
|
||||
for (const [locale, catalog] of catalogs) {
|
||||
if (locale === 'en') continue
|
||||
for (const [key, message] of catalog) {
|
||||
const normalized = normalizeMessageTypography(message)
|
||||
if (normalized !== message) drift.push({ locale, key, message: normalized })
|
||||
}
|
||||
}
|
||||
|
||||
return drift
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { TARGET_CATALOGS, assertCrowdinTargetConfiguration } from './locale-conf
|
||||
import { findInvalidTargetMessages, flattenCatalog, validateCatalogs } from './locale-catalog-validation.mjs'
|
||||
import { collectSourceMessageKeys } from './locale-source-keys.mjs'
|
||||
import { PROTECTED_SOURCE_TERMS, findProtectedTermDrift } from './locale-protected-terms.mjs'
|
||||
import { findTypographyDrift } from './locale-typography.mjs'
|
||||
|
||||
const API = 'https://api.crowdin.com/api/v2'
|
||||
const SOURCE_PATH_SUFFIX = '/client/src/locales/en.json'
|
||||
@@ -265,19 +266,31 @@ export function parseAllowedTranslationLosses(value = '') {
|
||||
// Exports are built with skipUntranslatedStrings, so Crowdin drops a key entirely once it stops
|
||||
// carrying a translation. Retention therefore compares presence, never message content: a translation
|
||||
// that equals the English source, such as Spanish "Error" or a product name, is a real translation.
|
||||
export function findTranslationLosses({ locale, reference, current, exported, rejected = new Map() }) {
|
||||
// A message this sync rejected is absent by our own decision and is reported as a rejection instead.
|
||||
export function findTranslationLosses({ locale, reference, current, exported, rejected = new Set() }) {
|
||||
const losses = []
|
||||
|
||||
for (const key of current.keys()) {
|
||||
if (!reference.has(key) || exported.has(key)) continue
|
||||
|
||||
const rejection = rejected.get(key)
|
||||
losses.push({ locale, key, reason: rejection ? `rejected by catalog validation - ${rejection[0]}` : 'missing from Crowdin export' })
|
||||
if (!reference.has(key) || exported.has(key) || rejected.has(key)) continue
|
||||
losses.push({ locale, key })
|
||||
}
|
||||
|
||||
return losses
|
||||
}
|
||||
|
||||
// Single translations leave Crowdin as a matter of course: a reviewer unapproves a string, or an
|
||||
// English edit invalidates the translation attached to it. Failing the whole sync on that churn
|
||||
// blocks every other locale over one message and demands a hand-typed acknowledgement that only a
|
||||
// manual run can supply, which is why this guard kept stalling the nightly export. It now watches for
|
||||
// the accident it was built to catch, an export that arrives empty or truncated, and lets ordinary
|
||||
// churn through to the pull request body.
|
||||
const RETENTION_LOSS_FLOOR = 25
|
||||
const RETENTION_LOSS_RATIO = 0.01
|
||||
|
||||
export function retentionLossLimit(translatedKeyCount) {
|
||||
return Math.max(RETENTION_LOSS_FLOOR, Math.ceil(translatedKeyCount * RETENTION_LOSS_RATIO))
|
||||
}
|
||||
|
||||
export function assertTranslationRetention({
|
||||
reference,
|
||||
currentCatalogs,
|
||||
@@ -287,31 +300,31 @@ export function assertTranslationRetention({
|
||||
rejections = [],
|
||||
}) {
|
||||
const rejectedByLocale = new Map()
|
||||
for (const { locale, key, errors } of rejections) {
|
||||
if (!rejectedByLocale.has(locale)) rejectedByLocale.set(locale, new Map())
|
||||
rejectedByLocale.get(locale).set(key, errors)
|
||||
for (const { locale, key } of rejections) {
|
||||
if (!rejectedByLocale.has(locale)) rejectedByLocale.set(locale, new Set())
|
||||
rejectedByLocale.get(locale).add(key)
|
||||
}
|
||||
|
||||
const losses = []
|
||||
const excessive = []
|
||||
for (const { locale } of targetCatalogs) {
|
||||
const current = currentCatalogs.get(locale)
|
||||
const exported = exportedCatalogs.get(locale)
|
||||
if (!current || !exported) throw new Error(`Translation retention comparison is missing the ${locale} catalog`)
|
||||
losses.push(...findTranslationLosses({ locale, reference, current, exported, rejected: rejectedByLocale.get(locale) }))
|
||||
|
||||
const detected = findTranslationLosses({ locale, reference, current, exported, rejected: rejectedByLocale.get(locale) })
|
||||
const unacknowledged = detected.filter(({ key }) => !allowedLosses.has(`${locale}:${key}`))
|
||||
const limit = retentionLossLimit(current.size)
|
||||
if (unacknowledged.length > limit) excessive.push({ locale, count: unacknowledged.length, limit })
|
||||
losses.push(...unacknowledged)
|
||||
}
|
||||
|
||||
const detected = new Set(losses.map(({ locale, key }) => `${locale}:${key}`))
|
||||
const unacknowledged = losses.filter(({ locale, key }) => !allowedLosses.has(`${locale}:${key}`))
|
||||
const unused = [...allowedLosses].filter((entry) => !detected.has(entry))
|
||||
if (unacknowledged.length === 0 && unused.length === 0) return
|
||||
if (excessive.length > 0) {
|
||||
const details = excessive.map(({ locale, count, limit }) => `${locale}: ${count} translations dropped, more than the ${limit} allowed`)
|
||||
throw new Error(`Crowdin export would lose existing translations:\n${details.join('\n')}`)
|
||||
}
|
||||
|
||||
const details = [
|
||||
...unacknowledged.slice(0, 25).map(({ locale, key, reason }) => `${locale}:${key} - ${reason}`),
|
||||
...unused.slice(0, 25).map((entry) => `${entry} - acknowledgement does not match an exported loss`),
|
||||
]
|
||||
const remaining = unacknowledged.length + unused.length - details.length
|
||||
if (remaining > 0) details.push(`...and ${remaining} more`)
|
||||
throw new Error(`Crowdin export would lose existing translations:\n${details.join('\n')}`)
|
||||
return losses
|
||||
}
|
||||
|
||||
const MAX_REPORTED_REJECTIONS = 50
|
||||
@@ -344,7 +357,32 @@ export function formatProtectedTermReport(corrections) {
|
||||
].join('\n')}\n`
|
||||
}
|
||||
|
||||
async function reportSyncIssues({ rejections, corrections, reportPath }) {
|
||||
export function formatTranslationLossReport(losses) {
|
||||
if (losses.length === 0) return ''
|
||||
|
||||
const listed = losses.slice(0, MAX_REPORTED_REJECTIONS)
|
||||
const lines = [
|
||||
`### Translations no longer in Crowdin (${losses.length})`,
|
||||
'',
|
||||
'Crowdin stopped returning a translation for these messages, so the English source renders instead.',
|
||||
'',
|
||||
...listed.map(({ locale, key }) => `- ${locale}: ${key}`),
|
||||
]
|
||||
if (losses.length > listed.length) lines.push(`- ...and ${losses.length - listed.length} more`)
|
||||
return `${lines.join('\n')}\n`
|
||||
}
|
||||
|
||||
async function reportSyncIssues({ rejections, corrections, normalizations, losses, reportPath }) {
|
||||
if (normalizations.length > 0) {
|
||||
console.log(`Normalized punctuation in ${normalizations.length} Crowdin messages`)
|
||||
}
|
||||
|
||||
if (losses.length > 0) {
|
||||
console.log(`Crowdin no longer translates ${losses.length} messages; the English source renders instead:`)
|
||||
for (const { locale, key } of losses.slice(0, MAX_REPORTED_REJECTIONS)) console.log(` ${locale}: ${key}`)
|
||||
if (losses.length > MAX_REPORTED_REJECTIONS) console.log(` ...and ${losses.length - MAX_REPORTED_REJECTIONS} more`)
|
||||
}
|
||||
|
||||
if (corrections.length > 0) {
|
||||
console.log(`Restored ${corrections.length} protected terms to the English source:`)
|
||||
for (const { locale, key, message } of corrections) console.log(` ${locale}: ${key} was "${message}"`)
|
||||
@@ -356,7 +394,9 @@ async function reportSyncIssues({ rejections, corrections, reportPath }) {
|
||||
if (rejections.length > MAX_REPORTED_REJECTIONS) console.log(` ...and ${rejections.length - MAX_REPORTED_REJECTIONS} more`)
|
||||
}
|
||||
|
||||
if (reportPath) await writeFile(reportPath, `${formatProtectedTermReport(corrections)}${formatRejectionReport(rejections)}`)
|
||||
if (reportPath) {
|
||||
await writeFile(reportPath, `${formatProtectedTermReport(corrections)}${formatRejectionReport(rejections)}${formatTranslationLossReport(losses)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncCrowdinTranslations({
|
||||
@@ -407,17 +447,20 @@ export async function syncCrowdinTranslations({
|
||||
const corrections = findProtectedTermDrift({ catalogs, terms: protectedTerms })
|
||||
for (const { locale, key, source } of corrections) catalogs.get(locale).set(key, source)
|
||||
|
||||
const normalizations = findTypographyDrift({ catalogs })
|
||||
for (const { locale, key, message } of normalizations) catalogs.get(locale).set(key, message)
|
||||
|
||||
const { slotCountKeys } = await collectMessageKeys()
|
||||
const rejections = findInvalidTargetMessages({ catalogs, slotCountKeys })
|
||||
for (const { locale, key } of rejections) catalogs.get(locale).delete(key)
|
||||
const rewrittenLocales = new Set([...rejections, ...corrections].map(({ locale }) => locale))
|
||||
const rewrittenLocales = new Set([...rejections, ...corrections, ...normalizations].map(({ locale }) => locale))
|
||||
for (const entry of downloaded) {
|
||||
if (rewrittenLocales.has(entry.locale)) entry.catalog = orderedSparseCatalog(reference, catalogs.get(entry.locale))
|
||||
}
|
||||
|
||||
const errors = validateCatalogs({ catalogs, slotCountKeys })
|
||||
if (errors.length > 0) throw new Error(`Crowdin export validation failed:\n${errors.join('\n')}`)
|
||||
assertTranslationRetention({
|
||||
const losses = assertTranslationRetention({
|
||||
reference: referenceMessages,
|
||||
currentCatalogs,
|
||||
exportedCatalogs: catalogs,
|
||||
@@ -430,9 +473,9 @@ export async function syncCrowdinTranslations({
|
||||
await Promise.all(
|
||||
downloaded.map(({ locale, catalog }) => writeFile(path.join(outputDirectory, `${locale}.json`), `${JSON.stringify(catalog, null, 2)}\n`)),
|
||||
)
|
||||
await reportSyncIssues({ rejections, corrections, reportPath })
|
||||
await reportSyncIssues({ rejections, corrections, normalizations, losses, reportPath })
|
||||
console.log(`Synchronized ${downloaded.length} sparse translation catalogs from Crowdin`)
|
||||
return { rejections, corrections }
|
||||
return { rejections, corrections, normalizations, losses }
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
findTranslationLosses,
|
||||
normalizeCrowdinCatalog,
|
||||
parseAllowedTranslationLosses,
|
||||
retentionLossLimit,
|
||||
sourceDrift,
|
||||
syncCrowdinTranslations,
|
||||
} from './sync-crowdin-translations.mjs'
|
||||
@@ -99,8 +100,8 @@ describe('Crowdin translation synchronization', () => {
|
||||
const exported = new Map()
|
||||
|
||||
expect(findTranslationLosses({ locale: 'cs', reference: referenceMessages, current, exported })).toEqual([
|
||||
{ locale: 'cs', key: 'common.save', reason: 'missing from Crowdin export' },
|
||||
{ locale: 'cs', key: 'common.cancel', reason: 'missing from Crowdin export' },
|
||||
{ locale: 'cs', key: 'common.save' },
|
||||
{ locale: 'cs', key: 'common.cancel' },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -112,7 +113,7 @@ describe('Crowdin translation synchronization', () => {
|
||||
const current = new Map([['common.save', 'Save']])
|
||||
|
||||
expect(findTranslationLosses({ locale: 'cs', reference: referenceMessages, current, exported: new Map() })).toEqual([
|
||||
{ locale: 'cs', key: 'common.save', reason: 'missing from Crowdin export' },
|
||||
{ locale: 'cs', key: 'common.save' },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -133,22 +134,16 @@ describe('Crowdin translation synchronization', () => {
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('reports why catalog validation dropped a key instead of a bare absence', () => {
|
||||
it('counts a message this sync rejected as a rejection rather than a lost translation', () => {
|
||||
expect(
|
||||
findTranslationLosses({
|
||||
locale: 'cs',
|
||||
reference: new Map([['common.save', 'Save']]),
|
||||
current: new Map([['common.save', 'Uložit']]),
|
||||
exported: new Map(),
|
||||
rejected: new Map([['common.save', ['cs: Unicode em dash is not allowed in common.save']]]),
|
||||
rejected: new Set(['common.save']),
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
locale: 'cs',
|
||||
key: 'common.save',
|
||||
reason: 'rejected by catalog validation - cs: Unicode em dash is not allowed in common.save',
|
||||
},
|
||||
])
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores catalog keys that English no longer defines', () => {
|
||||
@@ -168,42 +163,47 @@ describe('Crowdin translation synchronization', () => {
|
||||
const exportedCatalogs = new Map(TARGET_CATALOGS.map(({ locale }) => [locale, new Map([['bookDock.tab.error', 'Error']])]))
|
||||
currentCatalogs.set('es', new Map([['bookDock.tab.error', 'error']]))
|
||||
|
||||
expect(() => assertTranslationRetention({ reference: referenceMessages, currentCatalogs, exportedCatalogs })).not.toThrow()
|
||||
expect(() =>
|
||||
assertTranslationRetention({
|
||||
reference: referenceMessages,
|
||||
currentCatalogs,
|
||||
exportedCatalogs,
|
||||
allowedLosses: parseAllowedTranslationLosses('es:bookDock.tab.error'),
|
||||
}),
|
||||
).toThrow('es:bookDock.tab.error - acknowledgement does not match an exported loss')
|
||||
expect(assertTranslationRetention({ reference: referenceMessages, currentCatalogs, exportedCatalogs })).toEqual([])
|
||||
})
|
||||
|
||||
it('requires exact acknowledgements for intentional translation losses', () => {
|
||||
it('reports ordinary Crowdin churn instead of failing the whole export', () => {
|
||||
const referenceMessages = new Map([['common.save', 'Save']])
|
||||
const currentCatalogs = new Map(TARGET_CATALOGS.map(({ locale }) => [locale, new Map()]))
|
||||
const exportedCatalogs = new Map(TARGET_CATALOGS.map(({ locale }) => [locale, new Map()]))
|
||||
currentCatalogs.set('cs', new Map([['common.save', 'Uložit']]))
|
||||
|
||||
expect(() => assertTranslationRetention({ reference: referenceMessages, currentCatalogs, exportedCatalogs })).toThrow(
|
||||
'cs:common.save - missing from Crowdin export',
|
||||
)
|
||||
expect(() =>
|
||||
expect(assertTranslationRetention({ reference: referenceMessages, currentCatalogs, exportedCatalogs })).toEqual([
|
||||
{ locale: 'cs', key: 'common.save' },
|
||||
])
|
||||
expect(
|
||||
assertTranslationRetention({
|
||||
reference: referenceMessages,
|
||||
currentCatalogs,
|
||||
exportedCatalogs,
|
||||
allowedLosses: parseAllowedTranslationLosses('cs:common.save'),
|
||||
}),
|
||||
).not.toThrow()
|
||||
expect(() =>
|
||||
assertTranslationRetention({
|
||||
reference: referenceMessages,
|
||||
currentCatalogs,
|
||||
exportedCatalogs,
|
||||
allowedLosses: parseAllowedTranslationLosses('de:common.save'),
|
||||
}),
|
||||
).toThrow('de:common.save - acknowledgement does not match an exported loss')
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('fails when an export drops more of a catalog than churn explains', () => {
|
||||
const keys = Array.from({ length: 200 }, (_, index) => `common.key${index}`)
|
||||
const referenceMessages = new Map(keys.map((key) => [key, 'Save']))
|
||||
const currentCatalogs = new Map(TARGET_CATALOGS.map(({ locale }) => [locale, new Map()]))
|
||||
const exportedCatalogs = new Map(TARGET_CATALOGS.map(({ locale }) => [locale, new Map()]))
|
||||
currentCatalogs.set('cs', new Map(keys.map((key) => [key, 'Uložit'])))
|
||||
|
||||
expect(retentionLossLimit(200)).toBe(25)
|
||||
expect(() => assertTranslationRetention({ reference: referenceMessages, currentCatalogs, exportedCatalogs })).toThrow(
|
||||
'cs: 200 translations dropped, more than the 25 allowed',
|
||||
)
|
||||
exportedCatalogs.set('cs', new Map(keys.slice(0, 190).map((key) => [key, 'Uložit'])))
|
||||
expect(() => assertTranslationRetention({ reference: referenceMessages, currentCatalogs, exportedCatalogs })).not.toThrow()
|
||||
})
|
||||
|
||||
it('scales the retention limit with the size of the translated catalog', () => {
|
||||
expect(retentionLossLimit(0)).toBe(25)
|
||||
expect(retentionLossLimit(2500)).toBe(25)
|
||||
expect(retentionLossLimit(5640)).toBe(57)
|
||||
})
|
||||
|
||||
it('rejects malformed and duplicate loss acknowledgements', () => {
|
||||
@@ -348,7 +348,7 @@ describe('Crowdin translation synchronization', () => {
|
||||
const catalogDirectory = await createCatalogFixture(targetCatalogs)
|
||||
const outputDirectory = path.join(catalogDirectory, 'output')
|
||||
const reportPath = path.join(catalogDirectory, 'rejections.md')
|
||||
const catalogs = new Map([[targetCatalogs[0].languageId, { common: { save: 'Bad \u2014 value' } }]])
|
||||
const catalogs = new Map([[targetCatalogs[0].languageId, { common: { save: 'Bad <b>value</b>' } }]])
|
||||
|
||||
const { rejections } = await syncCrowdinTranslations({
|
||||
token: 'secret',
|
||||
@@ -365,55 +365,83 @@ describe('Crowdin translation synchronization', () => {
|
||||
{
|
||||
locale: targetCatalogs[0].locale,
|
||||
key: 'common.save',
|
||||
errors: [`${targetCatalogs[0].locale}: Unicode em dash is not allowed in common.save`],
|
||||
errors: [`${targetCatalogs[0].locale}: HTML is not allowed in common.save`],
|
||||
},
|
||||
])
|
||||
expect(JSON.parse(await readFile(path.join(outputDirectory, `${targetCatalogs[0].locale}.json`), 'utf8'))).toEqual({})
|
||||
expect(JSON.parse(await readFile(path.join(outputDirectory, `${targetCatalogs[1].locale}.json`), 'utf8'))).toEqual({
|
||||
common: { save: `Translated ${targetCatalogs[1].languageId}` },
|
||||
})
|
||||
expect(await readFile(reportPath, 'utf8')).toContain('Unicode em dash is not allowed in common.save')
|
||||
expect(await readFile(reportPath, 'utf8')).toContain('HTML is not allowed in common.save')
|
||||
})
|
||||
|
||||
it('fails when a rejected message would drop an existing translation', async () => {
|
||||
it('keeps exporting when a rejected message drops an existing translation', async () => {
|
||||
const targetCatalogs = TARGET_CATALOGS.slice(0, 2)
|
||||
const currentCatalogs = new Map([[targetCatalogs[0].locale, { common: { save: 'Ulozit' } }]])
|
||||
const catalogDirectory = await createCatalogFixture(targetCatalogs, currentCatalogs)
|
||||
const outputDirectory = path.join(catalogDirectory, 'output')
|
||||
const catalogs = new Map([[targetCatalogs[0].languageId, { common: { save: 'Bad \u2014 value' } }]])
|
||||
const catalogs = new Map([[targetCatalogs[0].languageId, { common: { save: 'Bad <b>value</b>' } }]])
|
||||
|
||||
await expect(
|
||||
syncCrowdinTranslations({
|
||||
token: 'secret',
|
||||
fetchImpl: createSynchronizationFetch({ catalogs }),
|
||||
catalogDirectory,
|
||||
outputDirectory,
|
||||
targetCatalogs,
|
||||
assertTargetConfiguration: async () => {},
|
||||
protectedTerms: [],
|
||||
}),
|
||||
).rejects.toThrow('rejected by catalog validation')
|
||||
await expect(access(outputDirectory)).rejects.toThrow()
|
||||
const { rejections, losses } = await syncCrowdinTranslations({
|
||||
token: 'secret',
|
||||
fetchImpl: createSynchronizationFetch({ catalogs }),
|
||||
catalogDirectory,
|
||||
outputDirectory,
|
||||
targetCatalogs,
|
||||
assertTargetConfiguration: async () => {},
|
||||
protectedTerms: [],
|
||||
})
|
||||
|
||||
expect(rejections).toHaveLength(1)
|
||||
expect(losses).toEqual([])
|
||||
expect(JSON.parse(await readFile(path.join(outputDirectory, `${targetCatalogs[0].locale}.json`), 'utf8'))).toEqual({})
|
||||
})
|
||||
|
||||
it('rewrites a Crowdin em dash instead of discarding the translation', async () => {
|
||||
const targetCatalogs = TARGET_CATALOGS.slice(0, 2)
|
||||
const currentCatalogs = new Map([[targetCatalogs[0].locale, { common: { save: 'Ulozit - hned' } }]])
|
||||
const catalogDirectory = await createCatalogFixture(targetCatalogs, currentCatalogs)
|
||||
const outputDirectory = path.join(catalogDirectory, 'output')
|
||||
const catalogs = new Map([[targetCatalogs[0].languageId, { common: { save: 'Ulozit \u2014 hned' } }]])
|
||||
|
||||
const { rejections, losses, normalizations } = await syncCrowdinTranslations({
|
||||
token: 'secret',
|
||||
fetchImpl: createSynchronizationFetch({ catalogs }),
|
||||
catalogDirectory,
|
||||
outputDirectory,
|
||||
targetCatalogs,
|
||||
assertTargetConfiguration: async () => {},
|
||||
protectedTerms: [],
|
||||
})
|
||||
|
||||
expect(rejections).toEqual([])
|
||||
expect(losses).toEqual([])
|
||||
expect(normalizations).toEqual([{ locale: targetCatalogs[0].locale, key: 'common.save', message: 'Ulozit - hned' }])
|
||||
expect(JSON.parse(await readFile(path.join(outputDirectory, `${targetCatalogs[0].locale}.json`), 'utf8'))).toEqual({
|
||||
common: { save: 'Ulozit - hned' },
|
||||
})
|
||||
})
|
||||
|
||||
it('checks translation retention before writing any files', async () => {
|
||||
const targetCatalogs = TARGET_CATALOGS.slice(0, 2)
|
||||
const currentCatalogs = new Map([[targetCatalogs[0].locale, { common: { save: 'Ulozit' } }]])
|
||||
const catalogDirectory = await createCatalogFixture(targetCatalogs, currentCatalogs)
|
||||
const keys = Array.from({ length: 40 }, (_, index) => `key${index}`)
|
||||
const referenceCatalog = { common: Object.fromEntries(keys.map((key) => [key, 'Save'])) }
|
||||
const currentCatalogs = new Map([[targetCatalogs[0].locale, { common: Object.fromEntries(keys.map((key) => [key, 'Ulozit'])) }]])
|
||||
const catalogDirectory = await createCatalogFixture(targetCatalogs, currentCatalogs, referenceCatalog)
|
||||
const outputDirectory = path.join(catalogDirectory, 'output')
|
||||
const catalogs = new Map([[targetCatalogs[0].languageId, {}]])
|
||||
const catalogs = new Map(targetCatalogs.map(({ languageId }) => [languageId, {}]))
|
||||
|
||||
await expect(
|
||||
syncCrowdinTranslations({
|
||||
token: 'secret',
|
||||
fetchImpl: createSynchronizationFetch({ catalogs }),
|
||||
fetchImpl: createSynchronizationFetch({ identifiers: keys.map((key) => `common.${key}`), catalogs }),
|
||||
catalogDirectory,
|
||||
outputDirectory,
|
||||
targetCatalogs,
|
||||
assertTargetConfiguration: async () => {},
|
||||
protectedTerms: [],
|
||||
}),
|
||||
).rejects.toThrow('missing from Crowdin export')
|
||||
).rejects.toThrow('40 translations dropped, more than the 25 allowed')
|
||||
await expect(access(outputDirectory)).rejects.toThrow()
|
||||
})
|
||||
|
||||
@@ -434,7 +462,7 @@ describe('Crowdin translation synchronization', () => {
|
||||
assertTargetConfiguration: async () => {},
|
||||
protectedTerms: [],
|
||||
}),
|
||||
).resolves.toEqual({ rejections: [], corrections: [] })
|
||||
).resolves.toEqual({ rejections: [], corrections: [], normalizations: [], losses: [] })
|
||||
expect(JSON.parse(await readFile(path.join(outputDirectory, `${targetCatalogs[0].locale}.json`), 'utf8'))).toEqual({
|
||||
common: { save: 'Save' },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user