Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/docs/CodeBlock.astro
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ const uid = Math.random().toString(36).slice(2);
<span class="font-mono text-[10px] text-violet-400/80 uppercase tracking-widest">{langLabel}</span>
<button
class="copy-btn flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-mono uppercase tracking-wider text-neutral-500 transition-colors hover:text-neutral-200 hover:bg-neutral-800/60"
aria-label={`Copiar ${langLabel} código`}
aria-label={`Copy ${langLabel} code`}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
</svg>
<span class="copy-label">Copiar</span>
<span class="copy-label">Copy</span>
</button>
</div>
<pre class="whitespace-pre"><code>{code}</code></pre>
Expand Down
2 changes: 1 addition & 1 deletion src/components/docs/LimitationsCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface Props {
items: Limitation[];
}

const { title = 'limitaciones conocidas', items } = Astro.props;
const { title = 'known limitations', items } = Astro.props;
---

<div class="rounded-xl border border-amber-900/40 bg-amber-950/20 p-6 mb-14 not-prose">
Expand Down
10 changes: 5 additions & 5 deletions src/components/docs/RateLimits.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ const { perKey, tokensPerMinuteByModel, requestsPerMinuteByModel, windowedModels

<div class="rounded-xl border border-neutral-800/60 bg-[#0a0a0a] p-6 mb-6">
<p class="font-mono text-[10px] text-violet-400 uppercase tracking-widest mb-4">
rate limits por API key
rate limits per API key
</p>
<dl class="grid gap-3 sm:grid-cols-2">
<div class="flex items-baseline justify-between gap-4 font-mono text-xs">
<dt class="text-neutral-500 uppercase tracking-wider">Requests / min</dt>
<dd class="text-neutral-200 text-right">{perKey.requestsPerMinute} rpm</dd>
</div>
<div class="flex items-baseline justify-between gap-4 font-mono text-xs">
<dt class="text-neutral-500 uppercase tracking-wider">Paralelo máximo</dt>
<dd class="text-neutral-200 text-right">{perKey.maxParallel} concurrentes</dd>
<dt class="text-neutral-500 uppercase tracking-wider">Max parallel</dt>
<dd class="text-neutral-200 text-right">{perKey.maxParallel} concurrent</dd>
</div>
</dl>
</div>
Expand Down Expand Up @@ -67,7 +67,7 @@ const { perKey, tokensPerMinuteByModel, requestsPerMinuteByModel, windowedModels

<div class="rounded-xl border border-neutral-800/60 bg-[#0a0a0a] p-6 mb-6">
<p class="font-mono text-[10px] text-violet-400 uppercase tracking-widest mb-4">
tokens / min por modelo
tokens / min per model
</p>
<dl class="grid gap-3 sm:grid-cols-2">
{
Expand All @@ -83,7 +83,7 @@ const { perKey, tokensPerMinuteByModel, requestsPerMinuteByModel, windowedModels

<div class="rounded-xl border border-neutral-800/60 bg-[#0a0a0a] p-6 mb-14">
<p class="font-mono text-[10px] text-violet-400 uppercase tracking-widest mb-4">
requests / min por modelo
requests / min per model
</p>
<dl class="grid gap-3 sm:grid-cols-2">
{
Expand Down
14 changes: 7 additions & 7 deletions src/layouts/Docs.astro
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ const searchData = [
</div>

<!-- Copy toast notification -->
<div class="copy-toast" id="copy-toast">¡Copiado al portapapeles!</div>
<div class="copy-toast" id="copy-toast">Copied to clipboard</div>

<script define:vars={{ searchData }} is:inline>
// Search: filters by title, description and headings. No dependencies and
Expand Down Expand Up @@ -477,22 +477,22 @@ const searchData = [
const button = document.createElement('button');
button.type = 'button';
button.className = 'copy-btn';
button.setAttribute('aria-label', `Copiar ${lang.label}`);
button.innerHTML = '<span class="copy-label">Copiar</span>';
button.setAttribute('aria-label', `Copy ${lang.label}`);
button.innerHTML = '<span class="copy-label">Copy</span>';
button.addEventListener('click', async () => {
const text = pre.textContent || '';
try {
await navigator.clipboard.writeText(text);
const copyLabel = button.querySelector('.copy-label');
if (copyLabel) copyLabel.textContent = 'Copiado!';
showCopyToast('¡Copiado al portapapeles!');
if (copyLabel) copyLabel.textContent = 'Copied';
showCopyToast('Copied to clipboard');
window.setTimeout(() => {
const resetLabel = button.querySelector('.copy-label');
if (resetLabel) resetLabel.textContent = 'Copiar';
if (resetLabel) resetLabel.textContent = 'Copy';
}, 1500);
} catch (err) {
console.error('copy failed', err);
showCopyToast('Error al copiar');
showCopyToast('Copy failed');
}
});

Expand Down
8 changes: 4 additions & 4 deletions src/lib/__fixtures__/ratelimits.expected.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
**rate limits por API key**
**rate limits per API key**

- Requests / min: 60 rpm
- Paralelo máximo: 5 concurrentes
- Max parallel: 5 concurrent

**glm5.2 · premium tier limits**

Expand All @@ -12,13 +12,13 @@

400M tokens per rolling 4 hours is the limit a heavy coding-agent run reaches first, well before the allowance. Once you hit it, glm5.2 requests are rejected until the window slides forward: it is a rolling window, not a daily reset. The allowance counter goes back to zero when your billing period starts, and if you upgrade part-way into a period that first allowance is prorated to the share of the period you paid for.

**tokens / min por modelo**
**tokens / min per model**

- deepseek-v4-flash: 1.5M tpm
- mimo-v2.5: 1.5M tpm
- qwen3.6: 1.5M tpm
- gemma4: 1.5M tpm

**requests / min por modelo**
**requests / min per model**

- rerank: 1000 rpm
8 changes: 4 additions & 4 deletions src/lib/mdxToText.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('mdxToText rate limits', () => {
],
});
expect(out).toContain('- Requests / min: 120 rpm');
expect(out).toContain('- Paralelo máximo: 8 concurrentes');
expect(out).toContain('- Max parallel: 8 concurrent');
expect(out).toContain('- foo: 2M tpm');
expect(out).toContain('- bar: 500 rpm');
expect(out).toContain('**baz · premium tier limits**');
Expand All @@ -110,7 +110,7 @@ describe('mdxToText rate limits', () => {
it('defaults to the same numbers <RateLimits /> renders', async () => {
const out = await mdxToText(input);
expect(out).toContain('- Requests / min: 60 rpm');
expect(out).toContain('- Paralelo máximo: 5 concurrentes');
expect(out).toContain('- Max parallel: 5 concurrent');
// glm5.2 is gated by the window, not by a per-minute rate, and the docs
// had no row for it at all while the model was already being served.
expect(out).toContain('- Rolling 4h window: 400M tokens');
Expand All @@ -124,8 +124,8 @@ describe('mdxToText rate limits', () => {
requestsPerMinuteByModel: [],
windowedModels: [],
});
expect(out).not.toContain('tokens / min por modelo');
expect(out).not.toContain('requests / min por modelo');
expect(out).not.toContain('tokens / min per model');
expect(out).not.toContain('requests / min per model');
expect(out).not.toContain('premium tier limits');
});
});
Expand Down
10 changes: 5 additions & 5 deletions src/lib/mdxToText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ function modelCardToMd(node: MdxNode): string {
}

function limitationsCardToMd(node: MdxNode): string {
const title = String(getAttr(node, 'title') ?? 'limitaciones conocidas');
const title = String(getAttr(node, 'title') ?? 'known limitations');
const items = (getAttr(node, 'items') as Array<{ title?: string; body?: string }> | undefined) || [];

const lines: string[] = [`### ${title}`, ''];
Expand Down Expand Up @@ -346,10 +346,10 @@ function calloutToMd(node: MdxNode): string {

function rateLimitsToMd(config: RateLimitsConfig): string {
const lines = [
'**rate limits por API key**',
'**rate limits per API key**',
'',
`- Requests / min: ${config.perKey.requestsPerMinute} rpm`,
`- Paralelo máximo: ${config.perKey.maxParallel} concurrentes`,
`- Max parallel: ${config.perKey.maxParallel} concurrent`,
'',
];
for (const m of config.windowedModels) {
Expand All @@ -366,12 +366,12 @@ function rateLimitsToMd(config: RateLimitsConfig): string {
);
}
if (config.tokensPerMinuteByModel.length) {
lines.push('**tokens / min por modelo**', '');
lines.push('**tokens / min per model**', '');
for (const m of config.tokensPerMinuteByModel) lines.push(`- ${m.model}: ${m.label}`);
lines.push('');
}
if (config.requestsPerMinuteByModel.length) {
lines.push('**requests / min por modelo**', '');
lines.push('**requests / min per model**', '');
for (const m of config.requestsPerMinuteByModel) lines.push(`- ${m.model}: ${m.label}`);
lines.push('');
}
Expand Down
93 changes: 93 additions & 0 deletions src/tests/layouts/docsCopyIsEnglish.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, it } from 'vitest';
import { readFileSync, readdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';

/**
* The docs are English-only, and Spanish had leaked into what a reader sees.
*
* Not into the prose, which is written in English, but into the CHROME: the
* copy button said "Copiar", the toast "¡Copiado al portapapeles!", the rate
* limits card "Paralelo máximo / concurrentes", and a `<LimitationsCard>` with
* no explicit title fell back to "limitaciones conocidas". None of it was
* caught by review because the surrounding code and content were English; the
* strings were defaults and labels buried in components.
*
* The Spanish that legitimately remains is out of scope here: comments, which
* no reader sees, and the `es` branch of DocsTopBar, which is the translated
* chrome of /es/docs/api.
*/

const here = dirname(fileURLToPath(import.meta.url));
const componentsDir = resolve(here, '../../components/docs');

/** Words that only appear in Spanish, and never inside a URL or identifier. */
const SPANISH = [
'copiar',
'copiado',
'portapapeles',
'limitaciones',
'conocidas',
'paralelo',
'concurrentes',
'por modelo',
'por api key',
'anterior',
'siguiente',
'en esta página',
'búsqueda',
'página',
];

/**
* Only strings a reader can end up seeing: quoted literals and text between
* tags. Comments are deliberately excluded, since the repo keeps Spanish ones
* in the layouts and styles on purpose.
*/
function visibleText(source: string): string {
return source
.split('\n')
.filter((line) => !/^\s*(\/\/|\*|\/\*)/.test(line))
.join('\n');
}

/** Files that legitimately hold Spanish copy for the /es route. */
const BILINGUAL = ['DocsTopBar.astro', 'ApiReference.astro'];

const files = [
...readdirSync(componentsDir)
.filter((f) => f.endsWith('.astro'))
.map((f) => [`components/docs/${f}`, resolve(componentsDir, f)] as const),
['layouts/Docs.astro', resolve(here, '../../layouts/Docs.astro')] as const,
['lib/mdxToText.ts', resolve(here, '../../lib/mdxToText.ts')] as const,
];

describe('the docs chrome is in English', () => {
for (const [label, path] of files) {
it(`${label} shows no Spanish to the reader`, () => {
const text = visibleText(readFileSync(path, 'utf-8')).toLowerCase();
// These two carry the translated chrome and metadata of /es/docs/api on
// purpose, in an `es` branch beside the `en` one. Their Spanish is the
// point, not a leak.
if (BILINGUAL.some((f) => label.endsWith(f))) return;
const found = SPANISH.filter((w) => text.includes(w));
expect(found, `${label}: ${found.join(', ')}`).toEqual([]);
});
}
});

/**
* The card's default title lives in two places that have to agree: the
* component a person reads and the extractor that feeds /api/docs. They drifted
* once already over the rate limits, which is why rateLimits.ts exists.
*/
describe('LimitationsCard default title', () => {
it('matches between the component and the text extractor', () => {
const component = readFileSync(resolve(componentsDir, 'LimitationsCard.astro'), 'utf-8');
const extractor = readFileSync(resolve(here, '../../lib/mdxToText.ts'), 'utf-8');
const fromComponent = /title = '([^']+)'/.exec(component)?.[1];
const fromExtractor = /getAttr\(node, 'title'\) \?\? '([^']+)'/.exec(extractor)?.[1];
expect(fromComponent).toBeDefined();
expect(fromExtractor).toBe(fromComponent);
});
});
Loading