fix translator parser and move controls to sticky sidebar
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-03 09:34:52 +02:00
parent f4fdbff6ba
commit 3690c46436
11 changed files with 368 additions and 323 deletions
+313 -315
View File
@@ -228,7 +228,7 @@ function parseNamedAssignments(
): Array<{ name: string; value: unknown }> {
const values: Array<{ name: string; value: unknown }> = [];
const declarationRegex =
/(?:^|[\s;])(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=\s*/guy;
/(?:^|[\s;])(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=\s*/gu;
for (let cursor = 0; cursor < source.length;) {
declarationRegex.lastIndex = cursor;
@@ -895,327 +895,325 @@ export function Workbench({
{importWarning && <p className="notice">{importWarning}</p>}
{rawImportError && <p className="error">{rawImportError}</p>}
</section>
<div className="translator-grid">
<div className="translator-main">
<section className="panel workspace">
<div className="panel-heading">
<div>
<p className="eyebrow">Review strings</p>
<h2>Source and target side by side</h2>
</div>
<div className="actions">
<label className="field field-inline">
<span>Search in key/value</span>
<input
value={filter}
onChange={(event) => setFilter(event.target.value)}
placeholder="Search keys or text"
/>
</label>
<span className="count">
{formatCount(glossaryRows.length)} rows ·{" "}
{formatCount(totalMissing)} missing
</span>
</div>
</div>
<section className="panel workspace pair-selectors">
<div className="panel-heading">
<p className="eyebrow">Language pair</p>
<h2>Choose authoritative and target language</h2>
{rows.length === 0 ? (
<p className="notice">
Open a file to review and edit translation keys.
</p>
) : (
<div className="table-wrap">
<table className="review-grid">
<thead>
<tr>
<th>Key</th>
<th>
{effectiveSourceLanguage.toUpperCase()} (authoritative)
</th>
<th>{effectiveTargetLanguage.toUpperCase()} (review)</th>
<th>Suggestions</th>
</tr>
</thead>
<tbody>
{glossaryRows.map((row) => {
const suggestions = row.suggestions;
return (
<tr key={row.key}>
<td className="key-cell">{formatPath(row.key)}</td>
<td>
<label
className="sr-only"
htmlFor={`source-${row.key}`}
>
Authoritative {formatPath(row.key)}
</label>
<textarea
id={`source-${row.key}`}
rows={2}
value={row.source}
onChange={(event) =>
setCell(
effectiveSourceLanguage,
row.key,
event.target.value,
)
}
/>
</td>
<td>
<label
className="sr-only"
htmlFor={`target-${row.key}`}
>
Target {formatPath(row.key)}
</label>
<textarea
id={`target-${row.key}`}
rows={2}
value={row.target}
onChange={(event) =>
setCell(
effectiveTargetLanguage,
row.key,
event.target.value,
)
}
/>
<div className="row-actions">
<button
type="button"
onClick={() => copySourceToTarget(row.key)}
>
Copy source
</button>
<button
type="button"
onClick={() => replaceTarget(row.key, "")}
>
Clear target
</button>
</div>
</td>
<td>
{suggestions.length === 0 ? (
<p className="muted">No suggestion.</p>
) : (
<div className="chip-list">
{suggestions.map((entry) => (
<button
key={`${row.key}-${entry.id}`}
type="button"
className="chip"
onClick={() =>
setCell(
effectiveTargetLanguage,
row.key,
entry.targetText,
)
}
title={`${entry.sourceText} -> ${entry.targetText}`}
>
{entry.targetText}
</button>
))}
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
</div>
<div className="form-grid">
<label className="field">
<span>Authoritative language</span>
<select
value={effectiveSourceLanguage}
onChange={(event) =>
setWorkspace((current) => {
const nextSource = event.target.value;
const fallbackTarget = Object.keys(current.localeMap).find(
(lang) => lang !== nextSource,
);
return {
...current,
sourceLanguage: nextSource,
targetLanguage:
current.targetLanguage === nextSource
? (fallbackTarget ?? current.targetLanguage)
: current.targetLanguage,
};
})
}
>
<aside className="translator-sidebar">
<section className="panel workspace pair-selectors">
<div className="panel-heading">
<p className="eyebrow">Language selection</p>
<h2>Choose and manage languages</h2>
</div>
<div className="form-grid">
<label className="field">
<span>Authoritative language</span>
<select
value={effectiveSourceLanguage}
onChange={(event) =>
setWorkspace((current) => {
const nextSource = event.target.value;
const fallbackTarget = Object.keys(
current.localeMap,
).find((lang) => lang !== nextSource);
return {
...current,
sourceLanguage: nextSource,
targetLanguage:
current.targetLanguage === nextSource
? (fallbackTarget ?? current.targetLanguage)
: current.targetLanguage,
};
})
}
>
{languageOptions.map((lang) => (
<option value={lang} key={`source-${lang}`}>
{lang}
</option>
))}
</select>
</label>
<label className="field">
<span>Review target language</span>
<select
value={effectiveTargetLanguage}
onChange={(event) =>
setWorkspace((current) => ({
...current,
targetLanguage: event.target.value,
}))
}
>
{languageOptions
.filter((lang) => lang !== effectiveSourceLanguage)
.map((lang) => (
<option value={lang} key={`target-${lang}`}>
{lang}
</option>
))}
</select>
</label>
<label className="field">
<span>Add language</span>
<div className="field-group">
<input
value={newLanguageCode}
onChange={(event) => setNewLanguageCode(event.target.value)}
placeholder="eg. fr"
/>
<button type="button" onClick={addLanguage}>
Add
</button>
</div>
</label>
</div>
<ul className="language-list">
{languageOptions.map((lang) => (
<option value={lang} key={`source-${lang}`}>
{lang}
</option>
))}
</select>
</label>
<label className="field">
<span>Review target language</span>
<select
value={effectiveTargetLanguage}
onChange={(event) =>
setWorkspace((current) => ({
...current,
targetLanguage: event.target.value,
}))
}
>
{languageOptions
.filter((lang) => lang !== effectiveSourceLanguage)
.map((lang) => (
<option value={lang} key={`target-${lang}`}>
<li key={lang}>
<span>
{lang}
</option>
{lang === effectiveSourceLanguage && " (authoritative)"}
{lang === effectiveTargetLanguage && " (target)"}
</span>
<button type="button" onClick={() => removeLanguage(lang)}>
Remove
</button>
</li>
))}
</ul>
</section>
<section className="panel workspace glossary">
<div className="panel-heading">
<p className="eyebrow">Glossary support</p>
<h2>Pair-aware terms + history</h2>
</div>
<form
className="glossary-form"
onSubmit={(event) => {
event.preventDefault();
addGlossaryTerm();
}}
>
<label className="field">
<span>Source language</span>
<input
value={newTermSourceLang}
onChange={(event) => setNewTermSourceLang(event.target.value)}
/>
</label>
<label className="field">
<span>Target language</span>
<input
value={newTermTargetLang}
onChange={(event) => setNewTermTargetLang(event.target.value)}
/>
</label>
<label className="field">
<span>Source term</span>
<input
value={newTermSource}
onChange={(event) => setNewTermSource(event.target.value)}
placeholder="e.g. dashboard"
/>
</label>
<label className="field">
<span>Target term</span>
<input
value={newTermTarget}
onChange={(event) => setNewTermTarget(event.target.value)}
placeholder="e.g. dashboard"
/>
</label>
<button type="submit" className="primary">
Add glossary term
</button>
</form>
{glossary.length === 0 ? (
<p className="notice">
No glossary terms yet. Add some language-pair terms.
</p>
) : (
<ul className="term-list">
{glossary.map((entry) => (
<li key={entry.id}>
<div>
<strong>
{entry.sourceLanguage}→{entry.targetLanguage}
</strong>
<p>
{entry.sourceText} = {entry.targetText}
</p>
</div>
<button
type="button"
onClick={() => removeGlossaryTerm(entry.id)}
>
Remove
</button>
</li>
))}
</select>
</label>
<label className="field">
<span>Add language</span>
<div className="field-group">
<input
value={newLanguageCode}
onChange={(event) => setNewLanguageCode(event.target.value)}
placeholder="eg. fr"
/>
<button type="button" onClick={addLanguage}>
Add
</ul>
)}
</section>
<section className="panel workspace">
<div className="panel-heading">
<p className="eyebrow">Output</p>
<h2>Download or share reviewed bundle</h2>
</div>
<div className="actions">
<button type="button" onClick={exportOutput}>
Download JSON
</button>
<button
type="button"
onClick={() => void sendOutput()}
className="primary"
>
Send to tool
</button>
</div>
</label>
</div>
</section>
<section className="panel workspace">
<div className="panel-heading">
<div>
<p className="eyebrow">Review strings</p>
<h2>Source and target side by side</h2>
</div>
<div className="actions">
<label className="field field-inline">
<span>Search in key/value</span>
<input
value={filter}
onChange={(event) => setFilter(event.target.value)}
placeholder="Search keys or text"
/>
</label>
<span className="count">
{formatCount(glossaryRows.length)} rows ·{" "}
{formatCount(totalMissing)} missing
</span>
</div>
</div>
{rows.length === 0 ? (
<p className="notice">
Open a file to review and edit translation keys.
</p>
) : (
<div className="table-wrap">
<table className="review-grid">
<thead>
<tr>
<th>Key</th>
<th>
{effectiveSourceLanguage.toUpperCase()} (authoritative)
</th>
<th>{effectiveTargetLanguage.toUpperCase()} (review)</th>
<th>Suggestions</th>
</tr>
</thead>
<tbody>
{glossaryRows.map((row) => {
const suggestions = row.suggestions;
return (
<tr key={row.key}>
<td className="key-cell">{formatPath(row.key)}</td>
<td>
<label
className="sr-only"
htmlFor={`source-${row.key}`}
>
Authoritative {formatPath(row.key)}
</label>
<textarea
id={`source-${row.key}`}
rows={2}
value={row.source}
onChange={(event) =>
setCell(
effectiveSourceLanguage,
row.key,
event.target.value,
)
}
/>
</td>
<td>
<label
className="sr-only"
htmlFor={`target-${row.key}`}
>
Target {formatPath(row.key)}
</label>
<textarea
id={`target-${row.key}`}
rows={2}
value={row.target}
onChange={(event) =>
setCell(
effectiveTargetLanguage,
row.key,
event.target.value,
)
}
/>
<div className="row-actions">
<button
type="button"
onClick={() => copySourceToTarget(row.key)}
>
Copy source
</button>
<button
type="button"
onClick={() => replaceTarget(row.key, "")}
>
Clear target
</button>
</div>
</td>
<td>
{suggestions.length === 0 ? (
<p className="muted">No suggestion.</p>
) : (
<div className="chip-list">
{suggestions.map((entry) => (
<button
key={`${row.key}-${entry.id}`}
type="button"
className="chip"
onClick={() =>
setCell(
effectiveTargetLanguage,
row.key,
entry.targetText,
)
}
title={`${entry.sourceText} -> ${entry.targetText}`}
>
{entry.targetText}
</button>
))}
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
<section className="panel workspace glossary">
<div className="panel-heading">
<p className="eyebrow">Glossary support</p>
<h2>Pair-aware terms + history</h2>
</div>
<form
className="glossary-form"
onSubmit={(event) => {
event.preventDefault();
addGlossaryTerm();
}}
>
<label className="field">
<span>Source language</span>
<input
value={newTermSourceLang}
onChange={(event) => setNewTermSourceLang(event.target.value)}
/>
</label>
<label className="field">
<span>Target language</span>
<input
value={newTermTargetLang}
onChange={(event) => setNewTermTargetLang(event.target.value)}
/>
</label>
<label className="field">
<span>Source term</span>
<input
value={newTermSource}
onChange={(event) => setNewTermSource(event.target.value)}
placeholder="e.g. dashboard"
/>
</label>
<label className="field">
<span>Target term</span>
<input
value={newTermTarget}
onChange={(event) => setNewTermTarget(event.target.value)}
placeholder="e.g. dashboard"
/>
</label>
<button type="submit" className="primary">
Add glossary term
</button>
</form>
{glossary.length === 0 ? (
<p className="notice">
No glossary terms yet. Add some language-pair terms.
</p>
) : (
<ul className="term-list">
{glossary.map((entry) => (
<li key={entry.id}>
<div>
<strong>
{entry.sourceLanguage}→{entry.targetLanguage}
</strong>
<p>
{entry.sourceText} = {entry.targetText}
</p>
</div>
<button
type="button"
onClick={() => removeGlossaryTerm(entry.id)}
>
Remove
</button>
</li>
))}
</ul>
)}
</section>
<section className="panel workspace">
<div className="panel-heading">
<p className="eyebrow">Languages in workspace</p>
<h2>Add/remove languages</h2>
</div>
<ul className="language-list">
{languageOptions.map((lang) => (
<li key={lang}>
<span>
{lang}
{lang === effectiveSourceLanguage && " (authoritative)"}
{lang === effectiveTargetLanguage && " (target)"}
</span>
<button type="button" onClick={() => removeLanguage(lang)}>
Remove
</button>
</li>
))}
</ul>
</section>
<section className="panel workspace">
<div className="panel-heading">
<p className="eyebrow">Output</p>
<h2>Download or share reviewed bundle</h2>
</div>
<div className="actions">
<button type="button" onClick={exportOutput}>
Download JSON
</button>
<button
type="button"
onClick={() => void sendOutput()}
className="primary"
>
Send to tool
</button>
</div>
{status && <p className="notice">{status}</p>}
{incoming?.status !== "ready" && incoming?.error && (
<p className="error">{incoming.error}</p>
)}
</section>
{status && <p className="notice">{status}</p>}
{incoming?.status !== "ready" && incoming?.error && (
<p className="error">{incoming.error}</p>
)}
</section>
</aside>
</div>
</main>
);
}