3 Commits
Author SHA1 Message Date
zemion 52314b9f90 chore(release): bump translator-tools to 0.1.3
Verify / verify (push) Canceled after 0s
2026-09-03 15:09:03 +02:00
zemion 62c723dc2f feat: filter translator rows by missing segments
Verify / verify (push) Canceled after 0s
2026-09-03 13:53:55 +02:00
zemion 3690c46436 fix translator parser and move controls to sticky sidebar
Verify / verify (push) Canceled after 0s
2026-09-03 09:34:52 +02:00
11 changed files with 392 additions and 325 deletions
+14
View File
@@ -6,6 +6,20 @@
- 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.3 - 2026-09-03
- Added a missing-segments filter in the translation review table.
## 0.1.2 - 2026-09-03
## 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
View File
@@ -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
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "translator-tools", "name": "translator-tools",
"version": "0.1.1", "version": "0.1.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "translator-tools", "name": "translator-tools",
"version": "0.1.1", "version": "0.1.3",
"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
View File
@@ -1,6 +1,6 @@
{ {
"name": "translator-tools", "name": "translator-tools",
"version": "0.1.1", "version": "0.1.3",
"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",
+14
View File
@@ -6,6 +6,20 @@
- 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.3 - 2026-09-03
- Added a missing-segments filter in the translation review table.
## 0.1.2 - 2026-09-03
## 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
View File
@@ -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
+1 -1
View File
@@ -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.3",
"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",
+103 -95
View File
@@ -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;
@@ -512,6 +512,7 @@ export function Workbench({
localeMap: { de: {} }, localeMap: { de: {} },
}); });
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const [showOnlyMissing, setShowOnlyMissing] = useState(false);
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [glossary, setGlossary] = useState<GlossaryTerm[]>(() => { const [glossary, setGlossary] = useState<GlossaryTerm[]>(() => {
try { try {
@@ -585,7 +586,7 @@ export function Workbench({
const target = workspace.localeMap[effectiveTargetLanguage] ?? {}; const target = workspace.localeMap[effectiveTargetLanguage] ?? {};
const allKeys = new Set([...Object.keys(source), ...Object.keys(target)]); const allKeys = new Set([...Object.keys(source), ...Object.keys(target)]);
const query = filter.trim().toLocaleLowerCase("en"); const query = filter.trim().toLocaleLowerCase("en");
const values = Array.from(allKeys) return Array.from(allKeys)
.sort((left, right) => left.localeCompare(right)) .sort((left, right) => left.localeCompare(right))
.map((path) => ({ .map((path) => ({
key: path, key: path,
@@ -599,10 +600,11 @@ export function Workbench({
row.source.toLocaleLowerCase("en").includes(query) || row.source.toLocaleLowerCase("en").includes(query) ||
row.target.toLocaleLowerCase("en").includes(query), row.target.toLocaleLowerCase("en").includes(query),
) )
.filter((row) => (!showOnlyMissing ? true : !row.target.trim()))
.slice(0, MAX_PATH_ROWS); .slice(0, MAX_PATH_ROWS);
return values;
}, [ }, [
filter, filter,
showOnlyMissing,
workspace.localeMap, workspace.localeMap,
effectiveSourceLanguage, effectiveSourceLanguage,
effectiveTargetLanguage, effectiveTargetLanguage,
@@ -895,77 +897,8 @@ 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">
<section className="panel workspace pair-selectors"> <div className="translator-main">
<div className="panel-heading">
<p className="eyebrow">Language pair</p>
<h2>Choose authoritative and target language</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>
</section>
<section className="panel workspace"> <section className="panel workspace">
<div className="panel-heading"> <div className="panel-heading">
<div> <div>
@@ -981,6 +914,14 @@ export function Workbench({
placeholder="Search keys or text" placeholder="Search keys or text"
/> />
</label> </label>
<label className="field field-inline">
<input
type="checkbox"
checked={showOnlyMissing}
onChange={(event) => setShowOnlyMissing(event.target.checked)}
/>
<span>Only missing segments</span>
</label>
<span className="count"> <span className="count">
{formatCount(glossaryRows.length)} rows ·{" "} {formatCount(glossaryRows.length)} rows ·{" "}
{formatCount(totalMissing)} missing {formatCount(totalMissing)} missing
@@ -1098,6 +1039,92 @@ export function Workbench({
</div> </div>
)} )}
</section> </section>
</div>
<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) => (
<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 glossary"> <section className="panel workspace glossary">
<div className="panel-heading"> <div className="panel-heading">
@@ -1173,27 +1200,6 @@ export function Workbench({
)} )}
</section> </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"> <section className="panel workspace">
<div className="panel-heading"> <div className="panel-heading">
<p className="eyebrow">Output</p> <p className="eyebrow">Output</p>
@@ -1216,6 +1222,8 @@ export function Workbench({
<p className="error">{incoming.error}</p> <p className="error">{incoming.error}</p>
)} )}
</section> </section>
</aside>
</div>
</main> </main>
); );
} }
+31
View File
@@ -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;
+1 -1
View File
@@ -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.3",
"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
View File
@@ -1 +1 @@
export const APP_VERSION = "0.1.1"; export const APP_VERSION = "0.1.3";