Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52314b9f90 | ||
|
|
62c723dc2f | ||
|
|
3690c46436 |
@@ -6,6 +6,20 @@
|
||||
- Added language pair selection, glossary storage and fuzzy in-context suggestions.
|
||||
- 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
|
||||
|
||||
- Improved parser robustness for JS-like translation files with multiple declarations.
|
||||
|
||||
@@ -33,7 +33,7 @@ npm run test:browser
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "translator-tools",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "translator-tools",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "0.3.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "translator-tools",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"description": "Review and translate i18next-like JSON language files locally.",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Albrecht Degering",
|
||||
|
||||
@@ -6,6 +6,20 @@
|
||||
- Added language pair selection, glossary storage and fuzzy in-context suggestions.
|
||||
- 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
|
||||
|
||||
- Improved parser robustness for JS-like translation files with multiple declarations.
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ npm run test:browser
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "de.add-ideas.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.",
|
||||
"entry": "./",
|
||||
"icon": "./favicon.svg",
|
||||
|
||||
+103
-95
@@ -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;
|
||||
@@ -512,6 +512,7 @@ export function Workbench({
|
||||
localeMap: { de: {} },
|
||||
});
|
||||
const [filter, setFilter] = useState("");
|
||||
const [showOnlyMissing, setShowOnlyMissing] = useState(false);
|
||||
const [status, setStatus] = useState("");
|
||||
const [glossary, setGlossary] = useState<GlossaryTerm[]>(() => {
|
||||
try {
|
||||
@@ -585,7 +586,7 @@ export function Workbench({
|
||||
const target = workspace.localeMap[effectiveTargetLanguage] ?? {};
|
||||
const allKeys = new Set([...Object.keys(source), ...Object.keys(target)]);
|
||||
const query = filter.trim().toLocaleLowerCase("en");
|
||||
const values = Array.from(allKeys)
|
||||
return Array.from(allKeys)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
.map((path) => ({
|
||||
key: path,
|
||||
@@ -599,10 +600,11 @@ export function Workbench({
|
||||
row.source.toLocaleLowerCase("en").includes(query) ||
|
||||
row.target.toLocaleLowerCase("en").includes(query),
|
||||
)
|
||||
.filter((row) => (!showOnlyMissing ? true : !row.target.trim()))
|
||||
.slice(0, MAX_PATH_ROWS);
|
||||
return values;
|
||||
}, [
|
||||
filter,
|
||||
showOnlyMissing,
|
||||
workspace.localeMap,
|
||||
effectiveSourceLanguage,
|
||||
effectiveTargetLanguage,
|
||||
@@ -895,77 +897,8 @@ export function Workbench({
|
||||
{importWarning && <p className="notice">{importWarning}</p>}
|
||||
{rawImportError && <p className="error">{rawImportError}</p>}
|
||||
</section>
|
||||
|
||||
<section className="panel workspace pair-selectors">
|
||||
<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>
|
||||
|
||||
<div className="translator-grid">
|
||||
<div className="translator-main">
|
||||
<section className="panel workspace">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
@@ -981,6 +914,14 @@ export function Workbench({
|
||||
placeholder="Search keys or text"
|
||||
/>
|
||||
</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">
|
||||
{formatCount(glossaryRows.length)} rows ·{" "}
|
||||
{formatCount(totalMissing)} missing
|
||||
@@ -1098,6 +1039,92 @@ export function Workbench({
|
||||
</div>
|
||||
)}
|
||||
</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">
|
||||
<div className="panel-heading">
|
||||
@@ -1173,27 +1200,6 @@ export function Workbench({
|
||||
)}
|
||||
</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>
|
||||
@@ -1216,6 +1222,8 @@ export function Workbench({
|
||||
<p className="error">{incoming.error}</p>
|
||||
)}
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,27 @@ a {
|
||||
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,
|
||||
.panel {
|
||||
border: 1px solid var(--toolbox-border);
|
||||
@@ -369,6 +390,16 @@ a {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.translator-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.translator-sidebar {
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "de.add-ideas.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.",
|
||||
"entry": "./",
|
||||
"icon": "./favicon.svg",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const APP_VERSION = "0.1.1";
|
||||
export const APP_VERSION = "0.1.3";
|
||||
|
||||
Reference in New Issue
Block a user