This commit is contained in:
+313
-315
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user