snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
import fs from 'fs'
import path from 'path'
import { LokaliseApi, QueuedProcess } from '@lokalise/node-api'
// Pushes the English source strings to Lokalise and orders machine translations
// for anything still untranslated (see i18n-download-strings.ts for the other
// half of the pipeline).
//
// Two Lokalise projects are in play:
// - the dotcom project (LOKALISE_PROJECT_ID), source apps/dotcom/client/public/tla/locales/en.json
// - the tldraw SDK project (LOKALISE_TLDRAW_PROJECT_ID), source assets/translations/main.json
// Only the dotcom project orders translations automatically; SDK orders are
// still placed by hand.
const UPLOAD_POLL_INTERVAL_MS = 2000
const UPLOAD_MAX_POLLS = 120
/** Items per page when listing keys and languages, within Lokalise's per-endpoint maximums. */
const PAGE_LIMIT = 500
/** Anything pricier than this wants a human to look at it first. */
const MAX_ORDER_TOTAL = 10
function formatError(error: unknown) {
if (error instanceof Error) {
return error.stack ?? error.message
}
try {
return JSON.stringify(error, null, 2)
} catch {
return String(error)
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function getEnv(name: string) {
const value = process.env[name]
if (!value) throw new Error(`Missing required env var: ${name}`)
return value
}
/**
* Lokalise's file upload is asynchronous: it returns a queued process and
* creates the keys in the background. Poll until it's done — otherwise anything
* we ask about the project next still describes the state from before the
* upload, and freshly added strings look like they don't exist.
*/
async function waitForUpload(lokaliseApi: LokaliseApi, projectId: string, processId: string) {
for (let i = 0; i < UPLOAD_MAX_POLLS; i++) {
const queuedProcess = await lokaliseApi
.queuedProcesses()
.get(processId, { project_id: projectId })
if (queuedProcess.status === 'finished') return queuedProcess
if (queuedProcess.status === 'failed' || queuedProcess.status === 'cancelled') {
throw new Error(
`Upload ${queuedProcess.status}: ${queuedProcess.message || 'No message'}\n${JSON.stringify(queuedProcess, null, 2)}`
)
}
await sleep(UPLOAD_POLL_INTERVAL_MS)
}
throw new Error(`Upload timed out after ${UPLOAD_MAX_POLLS} polls for project ${projectId}`)
}
function logUploadResult(queuedProcess: QueuedProcess) {
const files = queuedProcess.details?.files ?? []
if (!files.length) {
console.log(' Upload finished, but Lokalise reported no file details.')
return
}
for (const file of files) {
console.log(
` ${file.name_original ?? 'file'}: ${file.key_count_inserted ?? 0} inserted, ` +
`${file.key_count_updated ?? 0} updated, ${file.key_count_skipped ?? 0} skipped ` +
`(${file.key_count_total ?? 0} total).`
)
}
}
async function uploadFile(
lokaliseApi: LokaliseApi,
options: { projectId: string; filePath: string; filename: string; cleanupMode: boolean }
) {
const { projectId, filePath, filename, cleanupMode } = options
const file = fs.readFileSync(filePath, 'utf8')
console.log(`Uploading ${filename}...`)
const uploadResult = await lokaliseApi.files().upload(projectId, {
data: Buffer.from(file).toString('base64'),
filename,
lang_iso: 'en',
detect_icu_plurals: true,
cleanup_mode: cleanupMode,
})
if (!uploadResult.process_id) {
throw new Error(`Upload did not return a process_id: ${JSON.stringify(uploadResult, null, 2)}`)
}
const queuedProcess = await waitForUpload(lokaliseApi, projectId, uploadResult.process_id)
console.log(`Uploaded ${filename} successfully!`)
logUploadResult(queuedProcess)
}
/** Every target language in the project, English excluded. Paginated: there are 50+ of them. */
async function listTargetLanguageIsos(lokaliseApi: LokaliseApi, projectId: string) {
const isos: string[] = []
for (let page = 1; ; page++) {
const result = await lokaliseApi
.languages()
.list({ project_id: projectId, limit: PAGE_LIMIT, page })
isos.push(...result.items.map((item) => item.lang_iso))
if (result.items.length < PAGE_LIMIT) break
}
return isos.filter((lang) => lang !== 'en')
}
/** Every key with at least one missing translation. Paginated: a big batch would otherwise truncate. */
async function listUntranslatedKeyIds(lokaliseApi: LokaliseApi, projectId: string) {
const keyIds: number[] = []
let cursor: string | undefined
do {
const result = await lokaliseApi.keys().list({
project_id: projectId,
filter_untranslated: 1,
pagination: 'cursor',
limit: PAGE_LIMIT,
...(cursor ? { cursor } : {}),
})
keyIds.push(...result.items.map((item) => item.key_id))
cursor = result.hasNextCursor() ? (result.nextCursor ?? undefined) : undefined
} while (cursor)
return keyIds
}
async function orderDotcomTranslations(lokaliseApi: LokaliseApi, projectId: string) {
const teamId = getEnv('LOKALISE_TEAM_ID')
const cardId = getEnv('LOKALISE_CARD_ID')
const targetLanguageIsos = await listTargetLanguageIsos(lokaliseApi, projectId)
const allProjectUntranslatedKeys = await listUntranslatedKeyIds(lokaliseApi, projectId)
if (!allProjectUntranslatedKeys.length) {
console.log('No new strings to translate.')
return
}
console.log(
`Found ${allProjectUntranslatedKeys.length} untranslated key(s) across ${targetLanguageIsos.length} language(s).`
)
const orderDetails = {
project_id: projectId,
payment_method: 'credit_card' as const,
card_id: String(cardId),
briefing: 'Please translate the following keys',
source_language_iso: 'en',
target_language_isos: targetLanguageIsos,
keys: allProjectUntranslatedKeys,
provider_slug: 'google',
translation_tier: 1,
}
console.log('Placing test order for new strings...')
const placeTranslationOrderDryRun = await lokaliseApi.orders().create(
{
...orderDetails,
dry_run: true,
},
{
team_id: teamId,
}
)
// Lokalise won't accept an order that prices out at 0, so there's nothing to
// place here. Log enough to diagnose it: the usual cause is that the keys are
// untranslated but not billable (already covered by translation memory, or
// sitting in an open order from a previous run).
if (placeTranslationOrderDryRun.total === 0) {
console.log(
`There are strings to translate but the dry run priced them at 0, so Lokalise won't take the order.`
)
console.log(
` Untranslated keys (${allProjectUntranslatedKeys.length}): ${allProjectUntranslatedKeys.join(', ')}`
)
console.log(
` Target languages (${targetLanguageIsos.length}): ${targetLanguageIsos.join(', ')}`
)
console.log(` Dry run response: ${JSON.stringify(placeTranslationOrderDryRun, null, 2)}`)
console.log('Place the order manually in Lokalise if these strings need translating.')
return
}
if (placeTranslationOrderDryRun.total > MAX_ORDER_TOTAL) {
console.error(
`Cost of translations (${placeTranslationOrderDryRun.total}) is exceeding expectations (${MAX_ORDER_TOTAL}). Place a manual order.`
)
process.exit(1)
}
console.log(
`Placing actual order for new strings (total: ${placeTranslationOrderDryRun.total})...`
)
await lokaliseApi.orders().create(
{
...orderDetails,
},
{
team_id: teamId,
}
)
console.log('Finished placing order for new strings.')
}
async function i18nUploadStrings() {
const lokaliseApi = new LokaliseApi({ apiKey: getEnv('LOKALISE_API_TOKEN') })
const dotcomProjectId = getEnv('LOKALISE_PROJECT_ID')
const tldrawProjectId = getEnv('LOKALISE_TLDRAW_PROJECT_ID')
await uploadFile(lokaliseApi, {
projectId: dotcomProjectId,
filePath: path.resolve(__dirname, '../../apps/dotcom/client/public/tla/locales/en.json'),
filename: 'en.json',
cleanupMode: true,
})
await orderDotcomTranslations(lokaliseApi, dotcomProjectId)
// The SDK project has no automated ordering — uploading keeps its source
// strings current so a manual order can pick them up. It runs last so a
// problem here can't stop the dotcom half of the pipeline.
// Cleanup mode deletes keys missing from the uploaded file, which is what
// keeps the project matching main.json. Both projects export with
// original_filenames, and i18n-download-strings.ts maps every file in a
// locale's folder to the same <locale>.json, so a stray file here doesn't
// show up as extra keys — it silently overwrites a locale's real strings
// depending on export order.
await uploadFile(lokaliseApi, {
projectId: tldrawProjectId,
filePath: path.resolve(__dirname, '../../assets/translations/main.json'),
filename: 'main.json',
cleanupMode: true,
})
}
i18nUploadStrings().catch((error) => {
console.error('Failed to upload i18n strings:')
console.error(formatError(error))
process.exit(1)
})