Skip to content

Commit 030ccb0

Browse files
feat(i18n): generate translations for 18 new docs locales
Enhance the materialize-missing-docs-translations script with per-locale processing, individual translation fallback, increased timeouts, and support for 18 additional target locales. Generate translated docs content for bg-BG, cs-CZ, da-DK, el-GR, es-419, fi-FI, hu-HU, id-ID, it-IT, nb-NO, nl-NL, pl-PL, pt-PT, ro-RO, sv-SE, th-TH, tr-TR, uk-UA, and vi-VN. Co-Authored-By: Hagicode <noreply@hagicode.com> Signed-off-by: newbe36524 <newbe36524@qq.com>
1 parent 93cc70c commit 030ccb0

840 files changed

Lines changed: 162831 additions & 53 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

scripts/materialize-missing-docs-translations.mjs

Lines changed: 114 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,34 @@ const targetLocales = [
2222
{ code: 'es-ES', translateCode: 'es' },
2323
{ code: 'pt-BR', translateCode: 'pt' },
2424
{ code: 'ru-RU', translateCode: 'ru' },
25+
{ code: 'it-IT', translateCode: 'it' },
26+
{ code: 'nl-NL', translateCode: 'nl' },
27+
{ code: 'pl-PL', translateCode: 'pl' },
28+
{ code: 'tr-TR', translateCode: 'tr' },
29+
{ code: 'sv-SE', translateCode: 'sv' },
30+
{ code: 'da-DK', translateCode: 'da' },
31+
{ code: 'fi-FI', translateCode: 'fi' },
32+
{ code: 'nb-NO', translateCode: 'no' },
33+
{ code: 'cs-CZ', translateCode: 'cs' },
34+
{ code: 'hu-HU', translateCode: 'hu' },
35+
{ code: 'ro-RO', translateCode: 'ro' },
36+
{ code: 'bg-BG', translateCode: 'bg' },
37+
{ code: 'el-GR', translateCode: 'el' },
38+
{ code: 'uk-UA', translateCode: 'uk' },
39+
{ code: 'vi-VN', translateCode: 'vi' },
40+
{ code: 'th-TH', translateCode: 'th' },
41+
{ code: 'id-ID', translateCode: 'id' },
42+
{ code: 'pt-PT', translateCode: 'pt' },
43+
{ code: 'es-419', translateCode: 'es' },
2544
];
2645
const separatorTemplate = '[[[TRSEP_%ID%]]]';
2746
const tokenPattern = /@@TR_[0-9]+@@/gu;
2847
const translatableFrontmatterKeys = new Set(['title', 'description']);
2948
const translatablePropKeys = new Set(['title', 'description']);
30-
const TRANSLATION_REQUEST_DELAY_MS = 350;
31-
const TRANSLATION_MAX_RETRIES = 6;
32-
const TRANSLATION_RETRY_BASE_DELAY_MS = 1500;
49+
const TRANSLATION_REQUEST_DELAY_MS = 800;
50+
const TRANSLATION_MAX_RETRIES = 8;
51+
const TRANSLATION_RETRY_BASE_DELAY_MS = 3000;
52+
const FETCH_TIMEOUT_MS = 60_000;
3353

3454
function sleep(milliseconds) {
3555
return new Promise((resolve) => {
@@ -69,7 +89,7 @@ async function collectBaselineDocs(currentDirectory, relativeDirectory = '') {
6989
if (entry.isDirectory()) {
7090
if (
7191
!relativeDirectory
72-
&& (isDocsLocaleDirectory(entry.name) || entry.name === 'blog' || entry.name === 'img')
92+
&& (isDocsLocaleDirectory(entry.name) || entry.name === 'img')
7393
) {
7494
continue;
7595
}
@@ -349,6 +369,35 @@ function decodeGoogleTranslateResponse(payload) {
349369
.join('');
350370
}
351371

372+
async function translateSingle(locale, translateCode, text) {
373+
const url = new URL('https://translate.googleapis.com/translate_a/single');
374+
url.searchParams.set('client', 'gtx');
375+
url.searchParams.set('sl', 'en');
376+
url.searchParams.set('tl', translateCode);
377+
url.searchParams.set('dt', 't');
378+
url.searchParams.set('q', text);
379+
380+
for (let attempt = 0; attempt <= TRANSLATION_MAX_RETRIES; attempt += 1) {
381+
if (attempt > 0) {
382+
const retryDelay = TRANSLATION_RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));
383+
await sleep(retryDelay);
384+
}
385+
386+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
387+
if (response.ok) {
388+
const payload = await response.json();
389+
return decodeGoogleTranslateResponse(payload);
390+
}
391+
392+
const shouldRetry = response.status === 429 || response.status >= 500;
393+
if (!shouldRetry || attempt === TRANSLATION_MAX_RETRIES) {
394+
throw new Error(`Translation request failed for ${locale}: ${response.status} ${response.statusText}`);
395+
}
396+
}
397+
398+
throw new Error(`Translation retries exhausted for ${locale}`);
399+
}
400+
352401
async function translateBatch(locale, translateCode, texts) {
353402
if (texts.length === 0) {
354403
return [];
@@ -370,7 +419,7 @@ async function translateBatch(locale, translateCode, texts) {
370419
await sleep(retryDelay);
371420
}
372421

373-
response = await fetch(url);
422+
response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
374423
if (response.ok) {
375424
break;
376425
}
@@ -385,54 +434,57 @@ async function translateBatch(locale, translateCode, texts) {
385434
const translated = decodeGoogleTranslateResponse(payload);
386435
const parts = translated.split(`\n${separator}\n`);
387436
if (parts.length !== texts.length) {
388-
throw new Error(`Unable to split translated payload for ${locale}`);
437+
console.warn(`Split failed for ${locale} batch (${texts.length} texts → ${parts.length} parts), falling back to individual translation`);
438+
const individual = [];
439+
for (const text of texts) {
440+
individual.push(await translateSingle(locale, translateCode, text));
441+
await sleep(TRANSLATION_REQUEST_DELAY_MS);
442+
}
443+
return individual;
389444
}
390445

391446
return parts;
392447
}
393448

394-
async function resolveTranslations(registry) {
449+
async function resolveTranslationsForLocale(targetLocale, registry) {
395450
const resolved = new Map();
451+
const entries = [...registry.entries.entries()].filter(([, entry]) => entry.locale === targetLocale.code);
452+
const batch = [];
453+
let batchLength = 0;
396454

397-
for (const targetLocale of targetLocales) {
398-
const entries = [...registry.entries.entries()].filter(([, entry]) => entry.locale === targetLocale.code);
399-
const batch = [];
400-
let batchLength = 0;
401-
402-
async function flush() {
403-
if (batch.length === 0) {
404-
return;
405-
}
406-
407-
const translatedParts = await translateBatch(
408-
targetLocale.code,
409-
targetLocale.translateCode,
410-
batch.map((entry) => entry.text),
411-
);
455+
async function flush() {
456+
if (batch.length === 0) {
457+
return;
458+
}
412459

413-
batch.forEach((entry, index) => {
414-
resolved.set(entry.token, translatedParts[index]);
415-
});
460+
const translatedParts = await translateBatch(
461+
targetLocale.code,
462+
targetLocale.translateCode,
463+
batch.map((entry) => entry.text),
464+
);
416465

417-
await sleep(TRANSLATION_REQUEST_DELAY_MS);
466+
batch.forEach((entry, index) => {
467+
resolved.set(entry.token, translatedParts[index]);
468+
});
418469

419-
batch.length = 0;
420-
batchLength = 0;
421-
}
470+
await sleep(TRANSLATION_REQUEST_DELAY_MS);
422471

423-
for (const [token, entry] of entries) {
424-
const projectedLength = batchLength + entry.text.length + 32;
425-
if (batch.length >= 24 || projectedLength >= 2800) {
426-
await flush();
427-
}
472+
batch.length = 0;
473+
batchLength = 0;
474+
}
428475

429-
batch.push({ token, text: entry.text });
430-
batchLength += entry.text.length + 32;
476+
for (const [token, entry] of entries) {
477+
const projectedLength = batchLength + entry.text.length + 32;
478+
if (batch.length >= 24 || projectedLength >= 2800) {
479+
await flush();
431480
}
432481

433-
await flush();
482+
batch.push({ token, text: entry.text });
483+
batchLength += entry.text.length + 32;
434484
}
435485

486+
await flush();
487+
436488
return resolved;
437489
}
438490

@@ -442,10 +494,12 @@ function materializeTemplate(template, translations) {
442494

443495
async function main() {
444496
const baselineDocs = await collectBaselineDocs(contentRoot);
445-
const registry = createTranslationRegistry();
446-
const pendingFiles = [];
497+
let totalCreated = 0;
447498

448499
for (const locale of targetLocales) {
500+
const localeRegistry = createTranslationRegistry();
501+
const localePendingFiles = [];
502+
449503
for (const relativeDocPath of baselineDocs) {
450504
const destinationPath = path.join(contentRoot, locale.code, relativeDocPath);
451505
const localizedOutputPath = path.join(translationsRoot, locale.code, relativeDocPath);
@@ -455,26 +509,33 @@ async function main() {
455509

456510
const englishSourcePath = path.join(englishRoot, relativeDocPath);
457511
const source = await fs.readFile(englishSourcePath, 'utf8');
458-
const template = buildTranslatedTemplate(source, locale.code, registry);
459-
pendingFiles.push({ locale: locale.code, relativeDocPath, destinationPath: localizedOutputPath, template });
512+
const template = buildTranslatedTemplate(source, locale.code, localeRegistry);
513+
localePendingFiles.push({ locale: locale.code, relativeDocPath, destinationPath: localizedOutputPath, template });
460514
}
461-
}
462515

463-
if (pendingFiles.length === 0) {
464-
console.log('No missing locale docs detected.');
465-
return;
466-
}
516+
if (localePendingFiles.length === 0) {
517+
console.log(`${locale.code}: no missing docs.`);
518+
continue;
519+
}
467520

468-
const translations = await resolveTranslations(registry);
521+
try {
522+
const translations = await resolveTranslationsForLocale(locale, localeRegistry);
469523

470-
for (const file of pendingFiles) {
471-
const content = materializeTemplate(file.template, translations);
472-
await fs.mkdir(path.dirname(file.destinationPath), { recursive: true });
473-
await fs.writeFile(file.destinationPath, content, 'utf8');
474-
console.log(`created ${file.locale}/${toPosixPath(file.relativeDocPath)}`);
524+
for (const file of localePendingFiles) {
525+
const content = materializeTemplate(file.template, translations);
526+
await fs.mkdir(path.dirname(file.destinationPath), { recursive: true });
527+
await fs.writeFile(file.destinationPath, content, 'utf8');
528+
console.log(`created ${file.locale}/${toPosixPath(file.relativeDocPath)}`);
529+
}
530+
531+
console.log(`${locale.code}: created ${localePendingFiles.length} docs.`);
532+
totalCreated += localePendingFiles.length;
533+
} catch (error) {
534+
console.error(`${locale.code}: FAILED - ${error.message}`);
535+
}
475536
}
476537

477-
console.log(`Created ${pendingFiles.length} locale docs.`);
538+
console.log(`Total created: ${totalCreated} locale docs.`);
478539
}
479540

480541
main().catch((error) => {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
title: Представяне на екипа за приключения
3+
description: Научете как работното пространство на Adventure Team на Hagicode управлява планирането на списък, потока на сътрудничество и обхвата.
4+
---
5+
6+
import { CardGrid, LinkCard } from '@astrojs/starlight/components';
7+
8+
Работното пространство на Adventure Team предлага планиране на списък с герои, настройка на натоварването и напредък в подземието на една повърхност. Тази страница остава с основните неща, от които читателите се нуждаят първо: какво прави работното пространство, как работи потокът и къде се вписва в ежедневната употреба.
9+
10+
## Преглед
11+
12+
- Работното пространство комбинира „кой трябва да изпълни задачата“, „коя конфигурация трябва да използва“ и „кое подземие се движи в момента“ на едно място.
13+
- Текущият комплект екранни снимки се фокусира върху две отговорности: повърхността на член/списък и потокът на сътрудничество между картите на подземието и редактирането на списъка.
14+
- Работи най-добре по време на изпълнение на предложение, координиране на групова задача и превключване на герой-роля, преди изпълнението да продължи.
15+
- Това не е страница за повторение на битка; това е контролна зала за свързване на намерението на подземието със списъка, който ще го изпълни.
16+
17+
## Работно пространство за членове и списък
18+
19+
![Списък с герои на Adventure Team и табло за управление](../img/screenshots/shared/screenshot-a1e8035a/original.png)
20+
21+
Тази екранна снимка е най-добрата опора за обяснение на самото работно пространство на списъка. Решетката със списък отляво помага на потребителите да сканират наличните герои, централният панел с детайли показва текущо избрания член, а настройките от дясната страна се фокусират върху бойната готовност плюс корекциите на професията/товара.
22+
23+
На практика потребителите обикновено се движат през панела в следния ред:
24+
25+
1. Изберете героите, които трябва да участват в текущото бягане.
26+
2. Потвърдете състоянието, ролята и готовността на избрания член, преди да продължите.
27+
3. Настройте подробностите за професията или натоварването, така че избраният герой да влезе в следващия работен процес с правилните настройки по подразбиране.
28+
29+
В истински екип за доставка това е стъпката „проверка на персонала плюс оборудването“ преди началото на мисията.
30+
31+
## Поток на сътрудничество и планиране на подземия
32+
33+
![Редактор на списък с подземия на Adventure Team](../img/screenshots/shared/screenshot-11d59f8f/original.png)
34+
35+
Тази екранна снимка показва как намерението на подземието и присвояването на членове остават свързани в един и същ работен процес. Картите с предложения за подземия вляво представляват мисиите, които могат да бъдат напреднати, докато редакторът на списъка вдясно свързва действителните изпълнители с избрания контекст на подземието.
36+
37+
Можете да го прочетете като цикъл на сътрудничество в три стъпки:
38+
39+
1. Изберете картата на подземието или мисията, която трябва да се придвижи следващата.
40+
2. Коригирайте участващите членове, индикаторите за състояние и назначените герои в редактора на списъка.
41+
3. Продължете към изпълнение, свързано с предложение, AutoTask или Prompt, с вече подготвен състав.
42+
43+
Ето защо функцията Adventure Team има значение: тя не просто показва герои, тя поддържа контекста на задачата, заданието и готовността за изпълнение, свързани в едно четливо предаване.
44+
45+
## Обхват и практически сцени
46+
47+
### Какво решава това работно пространство
48+
49+
- Той намалява превключването на контекста, като показва намерението за подземието и списъка за изпълнение заедно.
50+
- Той създава контролна точка за преглед на членове и настройка на конфигурацията, преди автоматизацията да продължи.
51+
- Той дава на екипите визуален отговор на въпроса „кой притежава тази мисия в момента и защо“.
52+
53+
## Свързано четене
54+
55+
<CardGrid>
56+
<LinkCard
57+
title="@@TR_30@@"
58+
href="/bg-BG/quick-start/wizard-setup"
59+
описание="@@TR_33@@"
60+
/>
61+
<LinkCard
62+
title="@@TR_35@@"
63+
href="/bg-BG/quick-start/proposal-session"
64+
описание="@@TR_38@@"
65+
/>
66+
<LinkCard
67+
title="@@TR_40@@"
68+
href="/bg-BG/product-overview"
69+
описание="@@TR_43@@"
70+
/>
71+
</CardGrid>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
title: Ръководство за абонамент на Alibaba Cloud DashScope
3+
description: Защо Alibaba Cloud DashScope пасва на облачни екипи и как да продължите с настройката на Claude Code.
4+
sidebar_position: 30
5+
tags:
6+
- ai-service
7+
- claude-code
8+
- aliyun
9+
---
10+
11+
import { LinkCard, CardGrid } from '@astrojs/starlight/components';
12+
13+
## Позициониране
14+
15+
Alibaba Cloud DashScope е един от доставчиците, които в момента се препоръчват от предварително зададения набор от документи за Claude Code. Практичен избор за екипи, които вече работят в екосистемата на Alibaba Cloud и искат унифицирано управление на конзолата.
16+
17+
## Подходящ за
18+
19+
- Вече използвате акаунти или ресурси на Alibaba Cloud
20+
- Искате абонаментът и управлението на ключове да останат в една облачна конзола
21+
- Предпочитате модел с фиксирана карта за бързо включване
22+
23+
## Ключови неща, които трябва да забележите
24+
25+
- Текущите предварително зададени метаданни маркират Alibaba Cloud DashScope като препоръчан.
26+
- Той съпоставя всички нива на Claude към `glm-4.7` за лесен път за настройка.
27+
- Особено удобно е, ако вашият екип вече работи в работните процеси на Alibaba Cloud.
28+
29+
## Моментна снимка на интеграцията
30+
31+
| Артикул | Стойност |
32+
|------|-------|
33+
| Вписване за абонамент | [Отворете страницата за абонамент](https://www.aliyun.com/benefit/ai/aistar?userCode=vmx5szbq&clubBiz=subTask..12384055..10263..) |
34+
| Официални документи / конзола | [Отворете официалната страница](https://dashscope.aliyun.com) |
35+
| Основен URL адрес, съвместим с антропно | `https://coding.dashscope.aliyuncs.com/apps/anthropic` |
36+
| Препоръчителна авт | `ANTHROPIC_AUTH_TOKEN` |
37+
38+
## Картографиране на модела по подразбиране
39+
40+
| Клод ниво | Модел по подразбиране |
41+
|-------------|---------------|
42+
| Сонет | `glm-4.7` |
43+
| Опус | `glm-4.7` |
44+
| Хайку | `glm-4.7` |
45+
46+
::: съвет
47+
Цените, промоциите и подробностите за пакета могат да се променят с времето. Ако цената е основната ви грижа, потвърдете най-новата информация директно на страницата на доставчика.
48+
:::
49+
50+
## Предложени следващи стъпки
51+
52+
1. Потвърдете кое ниво на абонамент или път за активиране на API искате да използвате.
53+
2. Отворете записа на Claude Code в унифицирания CLI център на AI Agent и продължете с официалните документи за инсталиране и справка на Anthropic.
54+
3. Проверете връзката локално след настройката.
55+
56+
## Следваща стъпка
57+
58+
<CardGrid>
59+
60+
<LinkCard title="Назад към прегледа" href="/bg-BG/ai-service-subscriptions/"
61+
описание="@@TR_81@@"
62+
/>
63+
64+
<LinkCard title="Отворете официалния запис за инсталиране на Claude Code" href="/bg-BG/related-software-installation/ai-agent-cli/#claude-code"
65+
описание="@@TR_84@@"
66+
/>
67+
68+
<LinkCard title="Отворете страницата за абонамент" href="https://www.aliyun.com/benefit/ai/aistar?userCode=vmx5szbq&clubBiz=subTask..12384055..10263.."
69+
описание="@@TR_86@@"
70+
/>
71+
72+
</CardGrid>

0 commit comments

Comments
 (0)