This commit is contained in:
@@ -6,6 +6,14 @@
|
|||||||
- Added language pair selection, glossary storage and fuzzy in-context suggestions.
|
- Added language pair selection, glossary storage and fuzzy in-context suggestions.
|
||||||
- Added local-only import/export pipeline with portal handoff integration.
|
- Added local-only import/export pipeline with portal handoff integration.
|
||||||
|
|
||||||
|
## 0.1.2 - 2026-09-03
|
||||||
|
|
||||||
|
- Fixed parsing of multiple JS-like const/let/var translations in one file so
|
||||||
|
both locales are discovered (e.g. `const de = ...; const en = ...;`).
|
||||||
|
- Added language handling sidebar with merged language-selection/workspace controls.
|
||||||
|
- Kept glossary and output panels in the sticky right sidebar to reduce layout
|
||||||
|
drift while reviewing long translation lists.
|
||||||
|
|
||||||
## 0.1.1 - 2026-09-02
|
## 0.1.1 - 2026-09-02
|
||||||
|
|
||||||
- Improved parser robustness for JS-like translation files with multiple declarations.
|
- Improved parser robustness for JS-like translation files with multiple declarations.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ npm run test:browser
|
|||||||
|
|
||||||
## Release
|
## Release
|
||||||
|
|
||||||
`npm run release:artifact` creates a deterministic `release/translator-tools-0.1.0.zip` and checksum sidecar.
|
`npm run release:artifact` creates a deterministic `release/translator-tools-0.1.2.zip` and checksum sidecar.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "translator-tools",
|
"name": "translator-tools",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "translator-tools",
|
"name": "translator-tools",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.3.0",
|
"@add-ideas/toolbox-contract": "0.3.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "translator-tools",
|
"name": "translator-tools",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "Review and translate i18next-like JSON language files locally.",
|
"description": "Review and translate i18next-like JSON language files locally.",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"author": "Albrecht Degering",
|
"author": "Albrecht Degering",
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
- Added language pair selection, glossary storage and fuzzy in-context suggestions.
|
- Added language pair selection, glossary storage and fuzzy in-context suggestions.
|
||||||
- Added local-only import/export pipeline with portal handoff integration.
|
- Added local-only import/export pipeline with portal handoff integration.
|
||||||
|
|
||||||
|
## 0.1.2 - 2026-09-03
|
||||||
|
|
||||||
|
- Fixed parsing of multiple JS-like const/let/var translations in one file so
|
||||||
|
both locales are discovered (e.g. `const de = ...; const en = ...;`).
|
||||||
|
- Added language handling sidebar with merged language-selection/workspace controls.
|
||||||
|
- Kept glossary and output panels in the sticky right sidebar to reduce layout
|
||||||
|
drift while reviewing long translation lists.
|
||||||
|
|
||||||
## 0.1.1 - 2026-09-02
|
## 0.1.1 - 2026-09-02
|
||||||
|
|
||||||
- Improved parser robustness for JS-like translation files with multiple declarations.
|
- Improved parser robustness for JS-like translation files with multiple declarations.
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ npm run test:browser
|
|||||||
|
|
||||||
## Release
|
## Release
|
||||||
|
|
||||||
`npm run release:artifact` creates a deterministic `release/translator-tools-0.1.0.zip` and checksum sidecar.
|
`npm run release:artifact` creates a deterministic `release/translator-tools-0.1.2.zip` and checksum sidecar.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.translator-tools",
|
"id": "de.add-ideas.translator-tools",
|
||||||
"name": "Translator Tools",
|
"name": "Translator Tools",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "Review and edit translation bundles in the browser with glossary suggestions.",
|
"description": "Review and edit translation bundles in the browser with glossary suggestions.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
|
|||||||
+313
-315
@@ -228,7 +228,7 @@ function parseNamedAssignments(
|
|||||||
): Array<{ name: string; value: unknown }> {
|
): Array<{ name: string; value: unknown }> {
|
||||||
const values: Array<{ name: string; value: unknown }> = [];
|
const values: Array<{ name: string; value: unknown }> = [];
|
||||||
const declarationRegex =
|
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;) {
|
for (let cursor = 0; cursor < source.length;) {
|
||||||
declarationRegex.lastIndex = cursor;
|
declarationRegex.lastIndex = cursor;
|
||||||
@@ -895,327 +895,325 @@ export function Workbench({
|
|||||||
{importWarning && <p className="notice">{importWarning}</p>}
|
{importWarning && <p className="notice">{importWarning}</p>}
|
||||||
{rawImportError && <p className="error">{rawImportError}</p>}
|
{rawImportError && <p className="error">{rawImportError}</p>}
|
||||||
</section>
|
</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">
|
{rows.length === 0 ? (
|
||||||
<div className="panel-heading">
|
<p className="notice">
|
||||||
<p className="eyebrow">Language pair</p>
|
Open a file to review and edit translation keys.
|
||||||
<h2>Choose authoritative and target language</h2>
|
</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>
|
||||||
<div className="form-grid">
|
|
||||||
<label className="field">
|
<aside className="translator-sidebar">
|
||||||
<span>Authoritative language</span>
|
<section className="panel workspace pair-selectors">
|
||||||
<select
|
<div className="panel-heading">
|
||||||
value={effectiveSourceLanguage}
|
<p className="eyebrow">Language selection</p>
|
||||||
onChange={(event) =>
|
<h2>Choose and manage languages</h2>
|
||||||
setWorkspace((current) => {
|
</div>
|
||||||
const nextSource = event.target.value;
|
<div className="form-grid">
|
||||||
const fallbackTarget = Object.keys(current.localeMap).find(
|
<label className="field">
|
||||||
(lang) => lang !== nextSource,
|
<span>Authoritative language</span>
|
||||||
);
|
<select
|
||||||
return {
|
value={effectiveSourceLanguage}
|
||||||
...current,
|
onChange={(event) =>
|
||||||
sourceLanguage: nextSource,
|
setWorkspace((current) => {
|
||||||
targetLanguage:
|
const nextSource = event.target.value;
|
||||||
current.targetLanguage === nextSource
|
const fallbackTarget = Object.keys(
|
||||||
? (fallbackTarget ?? current.targetLanguage)
|
current.localeMap,
|
||||||
: current.targetLanguage,
|
).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) => (
|
{languageOptions.map((lang) => (
|
||||||
<option value={lang} key={`source-${lang}`}>
|
<li key={lang}>
|
||||||
{lang}
|
<span>
|
||||||
</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}
|
{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>
|
</ul>
|
||||||
</label>
|
)}
|
||||||
<label className="field">
|
</section>
|
||||||
<span>Add language</span>
|
|
||||||
<div className="field-group">
|
<section className="panel workspace">
|
||||||
<input
|
<div className="panel-heading">
|
||||||
value={newLanguageCode}
|
<p className="eyebrow">Output</p>
|
||||||
onChange={(event) => setNewLanguageCode(event.target.value)}
|
<h2>Download or share reviewed bundle</h2>
|
||||||
placeholder="eg. fr"
|
</div>
|
||||||
/>
|
<div className="actions">
|
||||||
<button type="button" onClick={addLanguage}>
|
<button type="button" onClick={exportOutput}>
|
||||||
Add
|
Download JSON
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void sendOutput()}
|
||||||
|
className="primary"
|
||||||
|
>
|
||||||
|
Send to tool
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
{status && <p className="notice">{status}</p>}
|
||||||
</div>
|
{incoming?.status !== "ready" && incoming?.error && (
|
||||||
</section>
|
<p className="error">{incoming.error}</p>
|
||||||
|
)}
|
||||||
<section className="panel workspace">
|
</section>
|
||||||
<div className="panel-heading">
|
</aside>
|
||||||
<div>
|
</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>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,27 @@ a {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.translator-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(16rem, 24rem);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-main,
|
||||||
|
.translator-sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0.9rem;
|
||||||
|
align-self: start;
|
||||||
|
max-height: calc(100vh - 1.2rem);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.hero,
|
.hero,
|
||||||
.panel {
|
.panel {
|
||||||
border: 1px solid var(--toolbox-border);
|
border: 1px solid var(--toolbox-border);
|
||||||
@@ -369,6 +390,16 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
.translator-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translator-sidebar {
|
||||||
|
position: static;
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.translator-tools",
|
"id": "de.add-ideas.translator-tools",
|
||||||
"name": "Translator Tools",
|
"name": "Translator Tools",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "Review and edit translation bundles in the browser with glossary suggestions.",
|
"description": "Review and edit translation bundles in the browser with glossary suggestions.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const APP_VERSION = "0.1.1";
|
export const APP_VERSION = "0.1.2";
|
||||||
|
|||||||
Reference in New Issue
Block a user