@mxml3gend/gloss 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/baseline.js +118 -0
- package/dist/cache.js +78 -0
- package/dist/cacheMetrics.js +120 -0
- package/dist/check.js +153 -25
- package/dist/config.js +79 -4
- package/dist/fs.js +105 -6
- package/dist/hooks.js +101 -0
- package/dist/index.js +262 -6
- package/dist/server.js +140 -10
- package/dist/translationTree.js +20 -0
- package/dist/ui/assets/index-BCr07xD_.js +21 -0
- package/dist/ui/assets/index-CjmLcA1x.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/usage.js +107 -6
- package/dist/usageScanner.js +108 -15
- package/dist/xliff.js +92 -0
- package/package.json +3 -2
- package/dist/ui/assets/index-CgyZVU2h.css +0 -1
- package/dist/ui/assets/index-DfgO64nU.js +0 -12
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--text-primary: #0f2343;--text-secondary: #3f5478;--text-muted: #5b7196;--surface-card: #ffffff;--surface-border: #d6e0f0;font-family:Manrope,Avenir Next,Segoe UI,sans-serif;line-height:1.45;font-weight:500;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,*:before,*:after{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;background:radial-gradient(circle at 8% 0%,rgba(255,186,132,.26),transparent 42%),radial-gradient(circle at 90% 18%,rgba(120,184,255,.28),transparent 40%),linear-gradient(180deg,#f3f7ff,#f8f1e8)}#root{width:100%}.gloss-app{--space-1: .5rem;--space-2: .75rem;--space-3: 1rem;--space-4: 1.5rem;--space-5: 2rem;--state-danger-text: #842a34;--state-danger-border: #edc7cc;--state-danger-bg: #fff5f6;--state-warning-text: #75511f;--state-warning-border: #ead5ab;--state-warning-bg: #fff9ef;--state-info-text: #2a4f85;--state-info-border: #c7d8f6;--state-info-bg: #eef4ff;--state-success-text: #1f6943;--state-success-border: #b9e0c8;--state-success-bg: #eef8f2;--state-missing-row-bg: #fffaf8;--state-partial-row-bg: #fffdf7;--state-highlight-row-bg: #f2f7ff;--state-missing-cell-bg: #fffdf9;--state-dirty-cell-bg: #f2f7ff;--state-dirty-missing-cell-bg: #fff9f0;--state-unused-bg: #f8f9fc;max-width:1360px;margin:0 auto;padding:var(--space-4) var(--space-3) calc(var(--space-5) + var(--space-2));color:var(--text-primary)}.workspace-shell{margin-top:var(--space-3);display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--space-2);align-items:start}.action-sidebar{position:sticky;top:calc(var(--space-4) + .25rem);border:1px solid #d7e2f5;border-radius:14px;background:linear-gradient(180deg,#f9fbff,#f3f7ff);padding:.5rem;display:grid;gap:.55rem;box-shadow:0 8px 18px #1f3a6914}.action-sidebar__group{display:grid;gap:.4rem}.action-sidebar__btn{width:2.35rem;height:2.35rem;border:1px solid #c7d6ee;border-radius:10px;background:#fff;color:#274d88;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background .12s ease,border-color .12s ease,transform .12s ease}.action-sidebar__btn svg{width:1.1rem;height:1.1rem}.action-sidebar__btn:hover{background:#edf4ff;border-color:#88a8db;transform:translateY(-.5px)}.action-sidebar__btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.action-sidebar__btn.is-active{background:#e7efff;border-color:#5b7fc5;color:#1e3f75}.action-sidebar__btn--saving{animation:sidebar-pulse 1.1s ease-in-out infinite}@keyframes sidebar-pulse{0%{box-shadow:0 0 #2957b13d}70%{box-shadow:0 0 0 8px #2957b100}to{box-shadow:0 0 #2957b100}}.app-header{position:sticky;top:0;z-index:20;padding:var(--space-3);border-radius:16px;border:1px solid #d9e3f6;background:linear-gradient(140deg,#fdfefff7,#f8fafffa 55%,#f4f8fffa);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);box-shadow:0 10px 24px #16264612}.app-header__main{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:var(--space-3);align-items:center}.app-header__brand{min-width:0}.app-header__logo{display:block;width:min(380px,100%);height:auto}.app-header__controls{display:grid;justify-items:end;gap:var(--space-2)}.app-header__stats{display:flex;flex-wrap:wrap;gap:var(--space-1);justify-self:center;align-items:center;margin-top:0}.stat-chip{display:flex;align-items:baseline;gap:.45rem;padding:.4rem .68rem;border-radius:999px;background:#fffffff5;border:1px solid rgba(21,37,66,.09)}.stat-chip span{font-size:.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}.stat-chip strong{font-size:1rem}.language-switch{display:inline-flex;align-items:center;gap:.45rem;padding:.32rem .4rem;border-radius:999px;border:1px solid var(--surface-border);background:#fffffff0}.support-cards{margin-top:var(--space-2);display:grid;gap:var(--space-1);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));opacity:.88}.support-card{border-radius:12px;border:1px solid #e2e9f7;background:#fbfcff;padding:var(--space-2) var(--space-3)}.support-card h2{margin:0;font-size:.95rem}.support-card p{margin:var(--space-1) 0 0;color:var(--text-secondary);font-size:.88rem;line-height:1.45}.editor-shell{margin-top:0;border-radius:18px;border:1px solid #dbe4f5;background:#fdfefe;padding:var(--space-3);box-shadow:0 16px 30px #0f234c12;display:grid;gap:var(--space-2)}.status-bar{margin:0;border-radius:12px;border:1px solid #dbe5f7;background:#f8fbff;padding:.625rem .75rem;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);flex-wrap:wrap}.status-bar__main{margin:0;font-weight:600;font-size:.9rem}.status-bar__main--error{color:var(--state-danger-text)}.status-bar__main--warning{color:var(--state-warning-text);display:flex;align-items:center;gap:var(--space-1);flex-wrap:wrap}.status-bar__main--success{color:var(--state-success-text)}.status-bar__main--info{color:var(--text-secondary)}.status-bar__meta{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.status-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:999px;border:1px solid #cad8f2;background:#f0f5ff;color:#29476f;font-size:.76rem;font-weight:600;padding:.18rem .5rem}.status-chip[type=button]{cursor:pointer}.status-chip--muted{background:#f6f8fc;border-color:#dde4f2;color:#4d6285}.status-chip--warning{background:var(--state-warning-bg);border-color:var(--state-warning-border);color:var(--state-warning-text)}.status-chip--info{background:var(--state-info-bg);border-color:var(--state-info-border);color:var(--state-info-text)}.status-chip__action{opacity:.85;font-size:.72rem;border-left:1px solid currentColor;padding-left:.4rem}.status-bar__details{width:100%;border:1px solid #e0e8f7;border-radius:10px;background:#fff;padding:.6rem .75rem}.status-bar__details strong{display:block;margin-bottom:.45rem;color:#2f4b75;font-size:.82rem}.status-bar__details p{margin:0;color:var(--text-secondary);font-size:.84rem}.status-bar__details ul{margin:0;padding-left:1rem;display:grid;gap:.3rem}.status-bar__details li{color:#35527c;font-size:.82rem;line-height:1.35}.loading-state{margin:20vh auto 0;max-width:280px;border-radius:12px;border:1px solid #d5e2fb;background:#f7faff;padding:var(--space-3);text-align:center;color:var(--text-secondary)}.notice{margin:0 0 .7rem;padding:.6rem .75rem;border-radius:10px;border:1px solid transparent}.notice--error{color:var(--state-danger-text);background:var(--state-danger-bg);border-color:var(--state-danger-border)}.notice--success{color:var(--state-success-text);background:var(--state-success-bg);border-color:var(--state-success-border)}.notice--warning{color:var(--state-warning-text);background:var(--state-warning-bg);border-color:var(--state-warning-border)}.notice--stale{color:var(--state-warning-text);background:#fff8f0;border-color:#ecd8b6;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}.editor-tabs{display:flex;gap:var(--space-1);margin:0;padding:.25rem;border:1px solid #e2e9f6;border-radius:11px;background:#f7faff}.translations-workspace{display:grid;gap:var(--space-2)}.workspace-content-shell{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-2);align-items:start}.workspace-content-shell--with-drawer{grid-template-columns:minmax(0,1fr) minmax(320px,420px)}.editor-controls{display:grid;gap:var(--space-1);padding:var(--space-1);border-radius:14px;border:1px solid #e1e8f6;background:linear-gradient(180deg,#f9fbff,#f6f9ff)}.toolbar{display:grid;gap:.55rem;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.toolbar__top{display:grid;grid-template-columns:auto minmax(280px,1fr) auto;align-items:center;gap:.55rem}.toolbar__mode-switch{display:inline-flex;align-items:center;gap:.4rem}.toolbar__mode-group{display:grid;gap:.28rem}.toolbar__mode-hint{margin:0;color:#5e7395;font-size:.76rem;line-height:1.3}.toolbar__dsl-hint{margin-top:.05rem;font-size:.74rem;color:#61789e}.toolbar__field{display:grid;gap:.32rem;min-width:160px}.toolbar__field--search{min-width:0}.toolbar__field--search input{width:100%;min-height:2.3rem}.toolbar__field span{font-size:.75rem;font-weight:600;color:#5f7394;text-transform:uppercase;letter-spacing:.06em}.toolbar__actions{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:.45rem}.toolbar__translate-progress{margin:0;font-size:.82rem;color:#506687}.toolbar__chips{display:flex;align-items:center;flex-wrap:wrap;gap:.45rem}.toolbar__token-list{display:flex;flex-wrap:wrap;gap:.4rem}.toolbar__token{display:inline-flex;align-items:center;gap:.35rem;border:1px solid #c8d8f2;border-radius:999px;background:#fff;color:#2b476f;padding:.22rem .52rem;font-size:.74rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;cursor:pointer}.toolbar__token:hover{background:#f4f8ff;border-color:#9eb8e6}.toolbar__dsl-toggle{border:1px solid #d1ddf3;border-radius:999px;background:#f7faff;color:#36537e;font-size:.72rem;font-weight:700;letter-spacing:.03em;padding:.28rem .52rem;cursor:help}.filter-chip{border:1px solid #d5e1f5;border-radius:999px;background:#f7faff;color:#274368;font-size:.78rem;font-weight:600;padding:.3rem .58rem;cursor:pointer}.filter-chip.is-active{border-color:#87a9e3;background:#e8f0ff;color:#1d3f79}.filter-chip:disabled{opacity:.58;cursor:not-allowed}.filter-chip--clear{background:#fff;color:#516b93;border-color:#c8d8f2}.toolbar__advanced{width:100%;border:1px solid #dbe5f7;border-radius:12px;background:#f8fbff;padding:.7rem;display:grid;gap:.6rem}.toolbar__advanced-top{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.5rem}.toolbar__advanced-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap}.toolbar__advanced-head strong{font-size:.84rem;color:#2f4b75}.toolbar__advanced-actions{display:flex;gap:.4rem;flex-wrap:wrap}.toolbar__advanced-empty{margin:0;color:var(--text-secondary);font-size:.84rem}.toolbar__git-warning{margin:0;font-size:.82rem;color:#8f3c45}.toolbar__rules{display:grid;gap:.45rem}.toolbar__rule{display:grid;grid-template-columns:minmax(140px,1fr) minmax(140px,1fr) minmax(140px,1fr) auto;gap:.45rem;align-items:end;padding:.55rem;border:1px solid #d9e4f8;border-radius:10px;background:#fff}.toolbar__sort{display:flex;align-items:end;gap:.45rem;flex-wrap:wrap;padding-top:.25rem;border-top:1px dashed #d4dff4}.add-key-form{display:flex;align-items:flex-end;gap:var(--space-1);flex-wrap:wrap;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.add-key-form input{min-width:260px;flex:1 1 280px}.add-key-form__error{margin:0;color:#8f3c45;font-size:.82rem;padding:0 .125rem}.editor-main{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-2);align-items:start}.editor-main--translate{grid-template-columns:minmax(0,1fr)}.editor-explorers{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-2)}.editor-content{min-width:0;border:1px solid #dfe8f6;border-radius:15px;background:#fcfdff;padding:var(--space-2);box-shadow:inset 0 1px #fff}.analysis-drawer{min-width:0;max-height:74vh;border:1px solid #d9e5f7;border-radius:14px;background:#f8fbff;padding:.6rem;display:grid;grid-template-rows:auto minmax(0,1fr);gap:.55rem;overflow:hidden}.analysis-drawer__header{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.analysis-drawer__title{margin:0;font-size:.82rem;color:var(--text-muted);font-weight:700;letter-spacing:.05em;text-transform:uppercase}.analysis-drawer__body{min-height:0;overflow:auto}.analysis-drawer .issues-panel,.analysis-drawer .duplicates-panel,.analysis-drawer .usage-details-panel{border:0;border-radius:0;background:transparent;padding:0}.usage-details-panel{display:grid;gap:.75rem}.usage-details-panel__meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:.5rem;margin:0}.usage-details-panel__meta div{border:1px solid #dbe5f7;border-radius:10px;background:#fff;padding:.5rem .6rem}.usage-details-panel__meta dt{margin:0;font-size:.72rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted)}.usage-details-panel__meta dd{margin:.18rem 0 0;color:var(--text-primary);overflow-wrap:anywhere;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.usage-details-panel__section{border:1px solid #dce7f9;border-radius:10px;background:#fff;padding:.55rem .65rem}.usage-details-panel__title{margin:0;color:#365179;font-size:.78rem;font-weight:700}.usage-details-panel__empty{margin:.5rem 0 0;color:var(--text-secondary);font-size:.84rem}.file-tree,.namespace-tree{border:1px solid #d8e3f4;border-radius:14px;background:linear-gradient(180deg,#f8faff,#f2f7ff);padding:var(--space-2);max-height:32vh;overflow:auto;box-shadow:inset 0 1px #fff}.namespace-tree__title{margin:0 0 var(--space-1);font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);position:sticky;top:-.5rem;z-index:2;background:linear-gradient(180deg,#f8faff,#f8fafff5);padding:.45rem 0 .35rem}.namespace-tree__all-btn,.namespace-tree__node{width:100%;text-align:left;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--text-primary);font:inherit;cursor:pointer}.namespace-tree__all-btn{padding:.42rem .54rem}.namespace-tree__row{display:grid;grid-template-columns:auto minmax(0,1fr);gap:.3rem;align-items:center}.namespace-tree__toggle{border:1px solid transparent;background:transparent;color:#50688f;cursor:pointer;border-radius:7px;width:1.4rem;height:1.4rem;padding:0;display:inline-flex;align-items:center;justify-content:center}.namespace-tree__node{display:flex;justify-content:space-between;align-items:center;gap:.45rem;padding:.34rem .46rem}.namespace-tree__name{overflow:hidden;text-overflow:ellipsis}.namespace-tree__meta{color:var(--text-muted);font-size:.7rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace}.namespace-tree__all-btn:hover,.namespace-tree__node:hover,.namespace-tree__toggle:hover{background:#e9f1ff}.namespace-tree__all-btn.is-selected,.namespace-tree__node.is-selected{border-color:#8caadd;background:#e4edff;box-shadow:inset 3px 0 #466fcf}.namespace-tree__list{list-style:none;margin:var(--space-1) 0 0;padding:0;display:grid;gap:.1rem}.namespace-tree__item{margin:0}.namespace-tree__empty{margin:.6rem 0 0;padding:.55rem .62rem;border:1px dashed #d8e4f6;border-radius:10px;background:#f8fbff;color:var(--text-secondary);font-size:.84rem;line-height:1.42}.file-tree__title{margin:0 0 var(--space-1);font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);position:sticky;top:-.5rem;z-index:2;background:linear-gradient(180deg,#f8faff,#f8fafff5);padding:.45rem 0 .35rem}.file-tree__all-btn,.file-tree__folder-btn,.file-tree__file-btn{width:100%;text-align:left;border:1px solid transparent;border-radius:9px;background:transparent;padding:.42rem .54rem;color:var(--text-primary);font:inherit;cursor:pointer}.file-tree__all-btn,.file-tree__file-btn{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.file-tree__folder-btn{display:inline-flex;align-items:center;gap:.38rem;color:#26456f;font-weight:600}.file-tree__caret{width:.8rem;color:#50688f}.file-tree__folder-name{overflow:hidden;text-overflow:ellipsis}.file-tree__all-btn:hover,.file-tree__folder-btn:hover,.file-tree__file-btn:hover{background:#e9f1ff}.file-tree__all-btn.is-selected,.file-tree__file-btn.is-selected{border-color:#8caadd;background:#e4edff;box-shadow:inset 3px 0 #466fcf}.file-tree__file-name{overflow:hidden;text-overflow:ellipsis}.file-tree__file-count{color:var(--text-muted);font-size:.72rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;background:#edf2fd;border:1px solid #d4e0f5;border-radius:999px;padding:.1rem .38rem}.file-tree__list{list-style:none;margin:var(--space-1) 0 0;padding:0;display:grid;gap:.1rem}.file-tree__item{margin:0}.file-tree__empty{margin:.6rem 0 0;padding:.55rem .62rem;border:1px dashed #d8e4f6;border-radius:10px;background:#f8fbff;color:var(--text-secondary);font-size:.84rem;line-height:1.42}.issues-panel{border:1px solid var(--surface-border);border-radius:12px;background:#f8fafe;padding:.75rem;display:grid;gap:.75rem}.issues-panel__header{display:flex;align-items:center;justify-content:space-between;gap:.65rem;flex-wrap:wrap}.issues-panel__actions{display:flex;flex-wrap:wrap;gap:.4rem}.issues-panel__title{margin:0;font-size:.95rem;font-weight:700}.issues-panel__count{color:var(--text-muted);font-size:.8rem;font-weight:600}.issues-panel__list{list-style:none;margin:0;padding:0;display:grid;gap:.5rem}.issue-row{display:grid;grid-template-columns:auto minmax(220px,max-content) minmax(0,1fr) auto;align-items:center;gap:.55rem;border:1px solid #e1e8f7;border-radius:10px;background:#fff;padding:.5rem .6rem}.issue-row__badge{display:inline-flex;align-items:center;border-radius:999px;border:1px solid #d0dcf3;background:#f5f8ff;color:#32527e;font-size:.7rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:.15rem .45rem}.issue-row__key{border:0;background:transparent;color:#1c4b98;font:inherit;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;text-align:left;padding:0;cursor:pointer;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}.issue-row__key--passive{color:#3f5578;text-decoration:none;cursor:default}.issue-row__meta{color:var(--text-secondary);font-size:.82rem;overflow-wrap:anywhere}.issue-row__actions{display:inline-flex;justify-content:flex-end;align-items:center;gap:.35rem;flex-wrap:wrap}.issue-row--missing{border-color:#eadbb8;background:#fffbf4}.issue-row--placeholder_mismatch{border-color:#ecd8b4;background:#fffaf3}.issue-row--invalid_key{border-color:#efcfd4;background:#fff8f9}.issue-row--unused{border-color:#dde4f2;background:var(--state-unused-bg)}.issue-row--hardcoded_text{border-color:#ead7c2;background:#fffaf6}.duplicates-panel{border:1px solid var(--surface-border);border-radius:12px;background:#f8fafe;padding:.7rem}.duplicates-panel__title{margin:0 0 .65rem;font-size:.95rem;font-weight:700}.duplicates-panel__list{display:grid;gap:.75rem}.duplicate-locale{border:1px solid #d7e2f7;border-radius:10px;background:#fff;padding:.55rem}.duplicate-locale__title{margin:0 0 .45rem;font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}.duplicate-group{border:1px solid #e3ebfb;border-radius:9px;padding:.5rem;margin-top:.45rem}.duplicate-group__top{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:.5rem;align-items:center}.duplicate-group__value{overflow-wrap:anywhere}.duplicate-group__count{font-size:.78rem;color:var(--text-muted)}.duplicate-group__keys{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.2rem;color:var(--text-secondary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.table-wrap{border-radius:12px;border:1px solid #d8e3f4;background:#fff;overflow:auto;max-height:76vh}.grid-table{width:100%;min-width:1080px;border-collapse:collapse}.grid-table th{text-align:left;padding:.62rem .66rem;border-bottom:1px solid #d9e3f5;background:#f7faff;position:sticky;top:0;z-index:2;color:#3a557f;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em}.grid-table td{padding:.56rem .6rem;border-bottom:1px solid #edf1f8;vertical-align:top}.grid-table tr.row-state--none>td{background:var(--state-missing-row-bg)}.grid-table tr.row-state--partial>td{background:var(--state-partial-row-bg)}.grid-table tr.row-state--highlighted>td{background:var(--state-highlight-row-bg)}.grid-table tr.row-state--none td.value-cell--dirty,.grid-table tr.row-state--partial td.value-cell--dirty{background:var(--state-dirty-cell-bg)}.grid-table tr.row-state--none td.value-cell--dirty-missing,.grid-table tr.row-state--partial td.value-cell--dirty-missing{background:var(--state-dirty-missing-cell-bg)}.grid-table tbody tr:not(.usage-files-row):not(.virtual-spacer):not(.namespace-group-row):hover>td{background-color:#f8fbff}.namespace-group-row td{padding:0;border-bottom:1px solid #dfe8f6;background:#f7faff}.namespace-group-toggle{width:100%;border:0;background:transparent;padding:.56rem .66rem;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.45rem;color:#2e4f84;font:inherit;font-weight:600;cursor:pointer;text-align:left}.namespace-group-toggle:hover{background:#edf4ff}.namespace-group-toggle__caret{color:#4d6b9d;width:.9rem}.namespace-group-toggle__label{font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.82rem}.namespace-group-toggle__count{color:var(--text-muted);font-size:.74rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace}.key-col{width:300px;min-width:300px;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.8rem;color:#1f3659;border-right:1px solid #d7e2f4;background-clip:padding-box}.key-col__label{display:inline;overflow-wrap:anywhere}.key-col__dirty-dot{margin-left:.25rem;color:#2a63c7;font-weight:700}.key-col--translate{border-right:1px solid #dce6f7}.key-col--unused{color:#677186}.key-col--file-selected{box-shadow:inset 2px 0 #4a74d9}.key-col--placeholder-warning{box-shadow:inset 0 -2px #d39a3f}.usage-col{width:106px;min-width:106px}.usage-cell{color:var(--text-secondary);white-space:nowrap;text-align:center}.usage-cell--unused{background:var(--state-unused-bg)}.usage-toggle{border:0;background:none;padding:0;font:inherit;color:#1848a3;cursor:pointer;text-decoration:underline;text-underline-offset:2px;font-weight:600}.usage-tag{font-size:.7rem;font-weight:600;color:#667289;text-transform:uppercase;letter-spacing:.03em}.usage-files-row td{background:#f8fbff;padding-top:.7rem;padding-bottom:.8rem}.virtual-spacer td{border-bottom:0;padding:0}.usage-files{font-size:.82rem;color:var(--text-secondary);border:1px solid #dde6f6;border-radius:10px;background:#fff;padding:.6rem .7rem}.usage-files strong{margin-right:.45rem;color:var(--text-primary)}.usage-files-list{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.26rem;color:var(--text-primary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.key-diff-block{margin-top:.7rem;padding-top:.55rem;border-top:1px dashed #d7e3f8}.key-diff-line{overflow-wrap:anywhere}.status-col{width:168px;min-width:168px;color:#4a5f80;font-size:.79rem;line-height:1.35}.status-col__summary{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}.status-col__changed{color:#2f5d9b;font-weight:600}.status-col__tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.2rem}.status-inline-tag{display:inline-flex;align-items:center;border-radius:999px;padding:.1rem .42rem;font-size:.7rem;font-weight:700;letter-spacing:.03em}.status-inline-tag--warning{color:var(--state-warning-text);background:var(--state-warning-bg);border:1px solid var(--state-warning-border)}.status-inline-tag--info{color:var(--state-info-text);background:var(--state-info-bg);border:1px solid var(--state-info-border)}.locale-col,.locale-cell{min-width:260px}.value-cell{background:transparent}.value-cell--missing{background:var(--state-missing-cell-bg)}.value-cell--dirty{background:var(--state-dirty-cell-bg)}.value-cell--dirty-missing{background:var(--state-dirty-missing-cell-bg)}.value-cell--active{box-shadow:inset 0 0 0 2px #2c5ecf42}.value-input{box-sizing:border-box;width:100%;resize:none;overflow:hidden;min-height:2.05rem;line-height:1.32;border:1px solid #d6e2f4;border-radius:9px;background:#fff;padding:.4rem .5rem;font:inherit;color:var(--text-primary);transition:border-color .12s ease,box-shadow .12s ease,background .12s ease}.value-input:focus{outline:none;border-color:#7ca2e0;box-shadow:0 0 0 2px #5586d824}.value-input--dirty{border-color:#0e5fd8;box-shadow:0 0 0 2px #0e5fd814}.actions-col{width:1%;min-width:0;white-space:nowrap;text-align:right}.row-actions{display:flex;gap:.4rem;flex-wrap:nowrap}.row-action-icon-btn{width:2rem;height:2rem;padding:0}.row-action-icon-btn svg{width:1.05rem;height:1.05rem}.rename-form{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center}.inline-error{color:#9a241f;width:100%;font-size:.82rem}.empty-state{margin:1rem auto;padding:2.1rem 1.35rem;border:1px dashed #dbe4f5;border-radius:12px;background:linear-gradient(180deg,#fbfdff,#f6f9ff);text-align:center;max-width:700px;color:var(--text-secondary);line-height:1.48;font-size:.92rem}.empty-state--workspace{margin:1.2rem auto;padding:2.4rem 1.5rem}.empty-state--panel{margin:0;max-width:none;text-align:left;padding:1rem .92rem;border-style:solid;border-color:#dce7f8;background:#fff;font-size:.86rem;line-height:1.42}.footer-actions{margin-top:var(--space-2);padding:var(--space-2);border-top:1px solid #e2e9f6;border-radius:12px;background:linear-gradient(180deg,#f9fbff,#f6f9ff);display:grid;grid-template-columns:minmax(0,1fr);align-items:start;gap:var(--space-2)}.footer-actions__meta{min-width:0}.footer-actions__summary{margin:0;color:var(--text-secondary);font-size:.86rem;line-height:1.42;max-width:68ch}.footer-actions__xliff{margin-top:.55rem;display:flex;align-items:end;flex-wrap:wrap;gap:.45rem}.footer-actions__xliff-label{font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}.footer-actions__xliff-field{display:grid;gap:.18rem}.footer-actions__xliff-field span{font-size:.7rem;color:var(--text-muted)}.footer-actions__file-input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.footer-actions__links{margin-top:.45rem;display:flex;flex-wrap:wrap;gap:.45rem}.btn{border-radius:10px;border:1px solid transparent;padding:.5rem .8rem;font-size:.9rem;font-weight:600;text-decoration:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:transform .12s ease,box-shadow .12s ease,background .12s ease}.btn:hover{transform:translateY(-.5px)}.btn:disabled{opacity:.55;cursor:not-allowed;transform:none}.btn--primary{color:#fff;background:linear-gradient(135deg,#245fdf,#1b49be);box-shadow:0 10px 18px #1f59d942}.btn--ghost{color:#1d355c;border-color:#c2d0ea;background:#f6f9ff}.btn--support{color:#fff;background:linear-gradient(135deg,#ff7849,#f14c4c);box-shadow:0 7px 16px #f14c4c3b}.btn--danger{color:#9c212a;border-color:#eec1c7;background:#fff6f7}.btn--small{padding:.4rem .6rem;font-size:.8rem}.is-active{border-color:#4d73d7;background:#eaf1ff}input,textarea,select{border-radius:9px;border:1px solid #c4d2ea;background:#fff;color:var(--text-primary);padding:.48rem .6rem;font:inherit}input:focus,textarea:focus,select:focus{outline:none;border-color:#1f5eff;box-shadow:0 0 0 3px #1f5eff24}.modal-overlay{position:fixed;inset:0;background:#0e162480;display:flex;align-items:center;justify-content:center;padding:1rem;z-index:1000}.modal-dialog{width:min(480px,100%);border-radius:14px;border:1px solid var(--surface-border);background:var(--surface-card);box-shadow:0 24px 44px #0e1f4847;padding:1rem;display:grid;gap:.75rem}.modal-dialog__message{margin:0;color:var(--text-primary);white-space:pre-line}.modal-dialog__actions{display:flex;justify-content:flex-end;gap:.5rem}.toast-stack{position:fixed;right:1rem;bottom:1rem;z-index:1100}.toast{margin:0;border-radius:10px;border:1px solid #bde0cb;background:#edf9f1;color:#1e6f45;padding:.55rem .7rem;font-size:.86rem;font-weight:600;box-shadow:0 10px 24px #10361f24}.toast--success{border-color:#bde0cb;background:#edf9f1;color:#1e6f45}@media(max-width:860px){.gloss-app{padding:var(--space-3) var(--space-2) calc(var(--space-5) + var(--space-2))}.workspace-shell{margin-top:var(--space-2);grid-template-columns:1fr}.action-sidebar{position:static;grid-template-columns:repeat(2,auto);justify-content:start;align-items:start}.action-sidebar__group{grid-auto-flow:column;grid-auto-columns:min-content}.app-header{position:static}.app-header__main{grid-template-columns:1fr}.app-header__controls{justify-items:start}.editor-shell{padding:var(--space-2)}.status-bar{align-items:flex-start}.editor-controls{padding:.5rem}.toolbar__top,.toolbar__actions,.toolbar__chips,.toolbar__field,.toolbar__field--search,.toolbar__rule{width:100%}.toolbar__top{grid-template-columns:1fr}.toolbar__rule{grid-template-columns:1fr;align-items:stretch}.toolbar__field input,.toolbar__field select,.toolbar__actions .btn,.add-key-form input{width:100%}.editor-main,.workspace-content-shell--with-drawer{grid-template-columns:1fr}.analysis-drawer{max-height:none}.editor-explorers{grid-template-columns:1fr}.namespace-tree,.file-tree{max-height:none}.footer-actions,.issue-row{grid-template-columns:1fr;align-items:stretch}.issue-row__actions{justify-content:flex-start}.toast-stack{left:.75rem;right:.75rem;bottom:.75rem}}
|
package/dist/ui/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
12
12
|
<meta name="apple-mobile-web-app-title" content="Gloss" />
|
|
13
13
|
<link rel="manifest" href="/site.webmanifest" />
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-BCr07xD_.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CjmLcA1x.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/dist/usage.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { updateCacheMetrics } from "./cacheMetrics.js";
|
|
3
4
|
import { createScanMatcher } from "./scanFilters.js";
|
|
4
5
|
import { extractTranslationKeys } from "./usageExtractor.js";
|
|
5
6
|
const SUPPORTED_EXTENSIONS = [
|
|
@@ -33,6 +34,66 @@ const hasSkippedPathSegment = (relativePath) => normalizePath(relativePath)
|
|
|
33
34
|
.split("/")
|
|
34
35
|
.some((segment) => SKIP_DIRECTORIES.has(segment));
|
|
35
36
|
const isSupportedFile = (name) => SUPPORTED_EXTENSIONS.some((extension) => name.endsWith(extension));
|
|
37
|
+
const fileSignature = (mtimeMs, size) => `${mtimeMs}:${size}`;
|
|
38
|
+
const keyUsageCache = new Map();
|
|
39
|
+
const mtimeFromEntry = (entry) => {
|
|
40
|
+
if (typeof entry.mtimeMs === "number" && Number.isFinite(entry.mtimeMs)) {
|
|
41
|
+
return entry.mtimeMs;
|
|
42
|
+
}
|
|
43
|
+
const parsed = Number.parseFloat(entry.signature.split(":")[0] ?? "");
|
|
44
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
45
|
+
};
|
|
46
|
+
const sizeFromEntry = (entry) => {
|
|
47
|
+
if (typeof entry.sizeBytes === "number" && Number.isFinite(entry.sizeBytes)) {
|
|
48
|
+
return entry.sizeBytes;
|
|
49
|
+
}
|
|
50
|
+
const parsed = Number.parseInt(entry.signature.split(":")[1] ?? "", 10);
|
|
51
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
52
|
+
};
|
|
53
|
+
const summarizeCacheEntries = (entries) => {
|
|
54
|
+
let fileCount = 0;
|
|
55
|
+
let totalSizeBytes = 0;
|
|
56
|
+
let oldestMtimeMs = null;
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
fileCount += 1;
|
|
59
|
+
totalSizeBytes += sizeFromEntry(entry);
|
|
60
|
+
const mtime = mtimeFromEntry(entry);
|
|
61
|
+
if (mtime !== null) {
|
|
62
|
+
oldestMtimeMs = oldestMtimeMs === null ? mtime : Math.min(oldestMtimeMs, mtime);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { fileCount, totalSizeBytes, oldestMtimeMs };
|
|
66
|
+
};
|
|
67
|
+
export const keyUsageCacheKey = (cfg) => {
|
|
68
|
+
const root = projectRoot();
|
|
69
|
+
const i18nDirectory = translationsDir(cfg);
|
|
70
|
+
return `${path.resolve(root)}::${path.resolve(i18nDirectory)}::${JSON.stringify(cfg.scan ?? {})}`;
|
|
71
|
+
};
|
|
72
|
+
export const getKeyUsageCacheStatus = (cfg) => {
|
|
73
|
+
const cacheKey = keyUsageCacheKey(cfg);
|
|
74
|
+
const bucket = keyUsageCache.get(cacheKey);
|
|
75
|
+
if (!bucket) {
|
|
76
|
+
return {
|
|
77
|
+
cacheKey,
|
|
78
|
+
fileCount: 0,
|
|
79
|
+
totalSizeBytes: 0,
|
|
80
|
+
oldestMtimeMs: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
cacheKey,
|
|
85
|
+
...summarizeCacheEntries(bucket.files.values()),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export const clearKeyUsageCache = () => {
|
|
89
|
+
const bucketCount = keyUsageCache.size;
|
|
90
|
+
let fileCount = 0;
|
|
91
|
+
for (const bucket of keyUsageCache.values()) {
|
|
92
|
+
fileCount += bucket.files.size;
|
|
93
|
+
}
|
|
94
|
+
keyUsageCache.clear();
|
|
95
|
+
return { bucketCount, fileCount };
|
|
96
|
+
};
|
|
36
97
|
const extractRelativeImports = (content) => {
|
|
37
98
|
const imports = new Set();
|
|
38
99
|
const importRegexes = [
|
|
@@ -102,7 +163,7 @@ const isPageFile = (relativePath) => {
|
|
|
102
163
|
isNextAppPage ||
|
|
103
164
|
isSvelteKitPage);
|
|
104
165
|
};
|
|
105
|
-
const collectFiles = async (directory, projectDir, shouldScanFile, cfg, files) => {
|
|
166
|
+
const collectFiles = async (directory, projectDir, shouldScanFile, cfg, previousFiles, nextFiles, files) => {
|
|
106
167
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
107
168
|
for (const entry of entries) {
|
|
108
169
|
if (entry.name.startsWith(".")) {
|
|
@@ -113,7 +174,7 @@ const collectFiles = async (directory, projectDir, shouldScanFile, cfg, files) =
|
|
|
113
174
|
if (SKIP_DIRECTORIES.has(entry.name)) {
|
|
114
175
|
continue;
|
|
115
176
|
}
|
|
116
|
-
await collectFiles(fullPath, projectDir, shouldScanFile, cfg, files);
|
|
177
|
+
await collectFiles(fullPath, projectDir, shouldScanFile, cfg, previousFiles, nextFiles, files);
|
|
117
178
|
continue;
|
|
118
179
|
}
|
|
119
180
|
if (!entry.isFile() || !isSupportedFile(entry.name)) {
|
|
@@ -126,16 +187,38 @@ const collectFiles = async (directory, projectDir, shouldScanFile, cfg, files) =
|
|
|
126
187
|
if (!shouldScanFile(relativePath)) {
|
|
127
188
|
continue;
|
|
128
189
|
}
|
|
190
|
+
const stat = await fs.stat(fullPath);
|
|
191
|
+
const signature = fileSignature(stat.mtimeMs, stat.size);
|
|
192
|
+
const cached = previousFiles?.get(relativePath);
|
|
193
|
+
if (cached && cached.signature === signature) {
|
|
194
|
+
nextFiles.set(relativePath, cached);
|
|
195
|
+
files.push({
|
|
196
|
+
filePath: fullPath,
|
|
197
|
+
relativePath,
|
|
198
|
+
keys: new Set(cached.keys),
|
|
199
|
+
imports: [...cached.imports],
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
129
203
|
const content = await fs.readFile(fullPath, "utf8");
|
|
204
|
+
const keys = extractTranslationKeys(content, fullPath, cfg.scan?.mode);
|
|
205
|
+
const imports = extractRelativeImports(content);
|
|
206
|
+
nextFiles.set(relativePath, {
|
|
207
|
+
signature,
|
|
208
|
+
keys,
|
|
209
|
+
imports,
|
|
210
|
+
mtimeMs: stat.mtimeMs,
|
|
211
|
+
sizeBytes: stat.size,
|
|
212
|
+
});
|
|
130
213
|
files.push({
|
|
131
214
|
filePath: fullPath,
|
|
132
215
|
relativePath,
|
|
133
|
-
keys: new Set(
|
|
134
|
-
imports
|
|
216
|
+
keys: new Set(keys),
|
|
217
|
+
imports,
|
|
135
218
|
});
|
|
136
219
|
}
|
|
137
220
|
};
|
|
138
|
-
export async function buildKeyUsageMap(cfg) {
|
|
221
|
+
export async function buildKeyUsageMap(cfg, options) {
|
|
139
222
|
const root = projectRoot();
|
|
140
223
|
const i18nDirectory = translationsDir(cfg);
|
|
141
224
|
const candidateRoots = [
|
|
@@ -146,6 +229,10 @@ export async function buildKeyUsageMap(cfg) {
|
|
|
146
229
|
path.join(root, "routes"),
|
|
147
230
|
];
|
|
148
231
|
const sourceRoots = Array.from(new Set(candidateRoots.filter((candidate) => path.resolve(candidate) !== root)));
|
|
232
|
+
const useCache = options?.useCache !== false;
|
|
233
|
+
const cacheKey = keyUsageCacheKey(cfg);
|
|
234
|
+
const previousCache = useCache ? keyUsageCache.get(cacheKey) : undefined;
|
|
235
|
+
const nextFileCache = new Map();
|
|
149
236
|
const files = [];
|
|
150
237
|
const shouldScanFile = createScanMatcher(cfg.scan);
|
|
151
238
|
for (const sourceRoot of sourceRoots) {
|
|
@@ -153,7 +240,21 @@ export async function buildKeyUsageMap(cfg) {
|
|
|
153
240
|
if (!stat?.isDirectory()) {
|
|
154
241
|
continue;
|
|
155
242
|
}
|
|
156
|
-
await collectFiles(sourceRoot, root, shouldScanFile, cfg, files);
|
|
243
|
+
await collectFiles(sourceRoot, root, shouldScanFile, cfg, previousCache?.files, nextFileCache, files);
|
|
244
|
+
}
|
|
245
|
+
if (useCache) {
|
|
246
|
+
keyUsageCache.set(cacheKey, { files: nextFileCache });
|
|
247
|
+
const summary = summarizeCacheEntries(nextFileCache.values());
|
|
248
|
+
try {
|
|
249
|
+
await updateCacheMetrics(projectRoot(), "keyUsage", {
|
|
250
|
+
cacheKey,
|
|
251
|
+
...summary,
|
|
252
|
+
updatedAt: new Date().toISOString(),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Non-fatal: cache metrics are observability only.
|
|
257
|
+
}
|
|
157
258
|
}
|
|
158
259
|
const fileByPath = new Map(files.map((file) => [path.resolve(file.filePath), file]));
|
|
159
260
|
const adjacency = new Map();
|
package/dist/usageScanner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { updateCacheMetrics } from "./cacheMetrics.js";
|
|
3
4
|
import { createScanMatcher } from "./scanFilters.js";
|
|
4
5
|
import { extractTranslationKeys } from "./usageExtractor.js";
|
|
5
6
|
const IGNORED_DIRECTORIES = new Set([
|
|
@@ -17,6 +18,7 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
17
18
|
const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
18
19
|
const projectRoot = () => process.env.INIT_CWD || process.cwd();
|
|
19
20
|
const normalizePath = (filePath) => filePath.split(path.sep).join("/");
|
|
21
|
+
const usageScannerCache = new Map();
|
|
20
22
|
const isScannableFile = (fileName) => SCANNED_EXTENSIONS.has(path.extname(fileName));
|
|
21
23
|
const hasIgnoredPathSegment = (relativePath) => normalizePath(relativePath)
|
|
22
24
|
.split("/")
|
|
@@ -42,9 +44,69 @@ export const inferUsageRoot = (cfg) => {
|
|
|
42
44
|
}
|
|
43
45
|
return parentDirectory;
|
|
44
46
|
};
|
|
45
|
-
export
|
|
46
|
-
const
|
|
47
|
-
|
|
47
|
+
export const usageScannerCacheKey = (rootDir, scan) => {
|
|
48
|
+
const normalizedRoot = path.resolve(rootDir);
|
|
49
|
+
return `${normalizedRoot}::${JSON.stringify(scan ?? {})}`;
|
|
50
|
+
};
|
|
51
|
+
const fileSignature = (mtimeMs, size) => `${mtimeMs}:${size}`;
|
|
52
|
+
const mtimeFromEntry = (entry) => {
|
|
53
|
+
if (typeof entry.mtimeMs === "number" && Number.isFinite(entry.mtimeMs)) {
|
|
54
|
+
return entry.mtimeMs;
|
|
55
|
+
}
|
|
56
|
+
const parsed = Number.parseFloat(entry.signature.split(":")[0] ?? "");
|
|
57
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
58
|
+
};
|
|
59
|
+
const sizeFromEntry = (entry) => {
|
|
60
|
+
if (typeof entry.sizeBytes === "number" && Number.isFinite(entry.sizeBytes)) {
|
|
61
|
+
return entry.sizeBytes;
|
|
62
|
+
}
|
|
63
|
+
const parsed = Number.parseInt(entry.signature.split(":")[1] ?? "", 10);
|
|
64
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
65
|
+
};
|
|
66
|
+
const summarizeCacheEntries = (entries) => {
|
|
67
|
+
let fileCount = 0;
|
|
68
|
+
let totalSizeBytes = 0;
|
|
69
|
+
let oldestMtimeMs = null;
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
fileCount += 1;
|
|
72
|
+
totalSizeBytes += sizeFromEntry(entry);
|
|
73
|
+
const mtime = mtimeFromEntry(entry);
|
|
74
|
+
if (mtime !== null) {
|
|
75
|
+
oldestMtimeMs = oldestMtimeMs === null ? mtime : Math.min(oldestMtimeMs, mtime);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { fileCount, totalSizeBytes, oldestMtimeMs };
|
|
79
|
+
};
|
|
80
|
+
export const getUsageScannerCacheStatus = (rootDir, scan) => {
|
|
81
|
+
const cacheKey = usageScannerCacheKey(rootDir, scan);
|
|
82
|
+
const bucket = usageScannerCache.get(cacheKey);
|
|
83
|
+
if (!bucket) {
|
|
84
|
+
return {
|
|
85
|
+
cacheKey,
|
|
86
|
+
fileCount: 0,
|
|
87
|
+
totalSizeBytes: 0,
|
|
88
|
+
oldestMtimeMs: null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
cacheKey,
|
|
93
|
+
...summarizeCacheEntries(bucket.files.values()),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
export const clearUsageScannerCache = () => {
|
|
97
|
+
const bucketCount = usageScannerCache.size;
|
|
98
|
+
let fileCount = 0;
|
|
99
|
+
for (const bucket of usageScannerCache.values()) {
|
|
100
|
+
fileCount += bucket.files.size;
|
|
101
|
+
}
|
|
102
|
+
usageScannerCache.clear();
|
|
103
|
+
return { bucketCount, fileCount };
|
|
104
|
+
};
|
|
105
|
+
export async function scanUsage(rootDir = projectRoot(), scan, options) {
|
|
106
|
+
const useCache = options?.useCache !== false;
|
|
107
|
+
const cacheKey = usageScannerCacheKey(rootDir, scan);
|
|
108
|
+
const previousCache = useCache ? usageScannerCache.get(cacheKey) : undefined;
|
|
109
|
+
const nextFiles = new Map();
|
|
48
110
|
const shouldScanFile = createScanMatcher(scan);
|
|
49
111
|
const scanDirectory = async (directory) => {
|
|
50
112
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
@@ -67,23 +129,54 @@ export async function scanUsage(rootDir = projectRoot(), scan) {
|
|
|
67
129
|
if (!shouldScanFile(relativePath)) {
|
|
68
130
|
continue;
|
|
69
131
|
}
|
|
132
|
+
const stat = await fs.stat(fullPath);
|
|
133
|
+
const signature = fileSignature(stat.mtimeMs, stat.size);
|
|
134
|
+
const cached = previousCache?.files.get(relativePath);
|
|
135
|
+
if (cached && cached.signature === signature) {
|
|
136
|
+
nextFiles.set(relativePath, cached);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
70
139
|
const source = await fs.readFile(fullPath, "utf8");
|
|
71
140
|
const keys = extractTranslationKeys(source, fullPath, scan?.mode);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const fileSet = seenFilesByKey.get(key);
|
|
79
|
-
if (fileSet && !fileSet.has(relativePath)) {
|
|
80
|
-
fileSet.add(relativePath);
|
|
81
|
-
usage[key].files.push(relativePath);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
141
|
+
nextFiles.set(relativePath, {
|
|
142
|
+
signature,
|
|
143
|
+
keys,
|
|
144
|
+
mtimeMs: stat.mtimeMs,
|
|
145
|
+
sizeBytes: stat.size,
|
|
146
|
+
});
|
|
84
147
|
}
|
|
85
148
|
};
|
|
86
149
|
await scanDirectory(rootDir);
|
|
150
|
+
if (useCache) {
|
|
151
|
+
usageScannerCache.set(cacheKey, { files: nextFiles });
|
|
152
|
+
const summary = summarizeCacheEntries(nextFiles.values());
|
|
153
|
+
try {
|
|
154
|
+
await updateCacheMetrics(projectRoot(), "usageScanner", {
|
|
155
|
+
cacheKey,
|
|
156
|
+
...summary,
|
|
157
|
+
updatedAt: new Date().toISOString(),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Non-fatal: cache metrics are observability only.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const usage = {};
|
|
165
|
+
const seenFilesByKey = new Map();
|
|
166
|
+
for (const [relativePath, fileData] of nextFiles.entries()) {
|
|
167
|
+
for (const key of fileData.keys) {
|
|
168
|
+
if (!usage[key]) {
|
|
169
|
+
usage[key] = { count: 0, files: [] };
|
|
170
|
+
seenFilesByKey.set(key, new Set());
|
|
171
|
+
}
|
|
172
|
+
usage[key].count += 1;
|
|
173
|
+
const fileSet = seenFilesByKey.get(key);
|
|
174
|
+
if (fileSet && !fileSet.has(relativePath)) {
|
|
175
|
+
fileSet.add(relativePath);
|
|
176
|
+
usage[key].files.push(relativePath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
87
180
|
for (const value of Object.values(usage)) {
|
|
88
181
|
value.files.sort((left, right) => left.localeCompare(right));
|
|
89
182
|
}
|
package/dist/xliff.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { flattenObject } from "./translationTree.js";
|
|
2
|
+
const escapeXml = (value) => value
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """)
|
|
7
|
+
.replace(/'/g, "'");
|
|
8
|
+
const decodeXml = (value) => value
|
|
9
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
|
|
10
|
+
.replace(/&#([0-9]+);/g, (_, num) => String.fromCodePoint(Number.parseInt(num, 10)))
|
|
11
|
+
.replace(/'/g, "'")
|
|
12
|
+
.replace(/"/g, '"')
|
|
13
|
+
.replace(/>/g, ">")
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/&/g, "&");
|
|
16
|
+
const normalizeXmlText = (value) => {
|
|
17
|
+
const cdataUnwrapped = value.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, inner) => inner);
|
|
18
|
+
const withoutTags = cdataUnwrapped.replace(/<[^>]*>/g, "");
|
|
19
|
+
return decodeXml(withoutTags);
|
|
20
|
+
};
|
|
21
|
+
const readTagContent = (block, tag) => {
|
|
22
|
+
const regex = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i");
|
|
23
|
+
const match = regex.exec(block);
|
|
24
|
+
if (!match) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
return normalizeXmlText(match[1]);
|
|
28
|
+
};
|
|
29
|
+
const readBlockId = (attrs) => {
|
|
30
|
+
const match = /\bid\s*=\s*["']([^"']+)["']/i.exec(attrs);
|
|
31
|
+
return match ? decodeXml(match[1].trim()) : "";
|
|
32
|
+
};
|
|
33
|
+
const collectBlocks = (xml, tagName) => {
|
|
34
|
+
const regex = new RegExp(`<${tagName}\\b([^>]*)>([\\s\\S]*?)<\\/${tagName}>`, "gi");
|
|
35
|
+
const blocks = [];
|
|
36
|
+
let match = regex.exec(xml);
|
|
37
|
+
while (match) {
|
|
38
|
+
const id = readBlockId(match[1] ?? "");
|
|
39
|
+
if (id) {
|
|
40
|
+
blocks.push({ id, content: match[2] ?? "" });
|
|
41
|
+
}
|
|
42
|
+
match = regex.exec(xml);
|
|
43
|
+
}
|
|
44
|
+
return blocks;
|
|
45
|
+
};
|
|
46
|
+
export const buildXliffDocument = ({ translations, locales, sourceLocale, targetLocale, }) => {
|
|
47
|
+
const flattenedByLocale = {};
|
|
48
|
+
for (const locale of locales) {
|
|
49
|
+
flattenedByLocale[locale] = flattenObject(translations[locale] ?? {});
|
|
50
|
+
}
|
|
51
|
+
const keySet = new Set();
|
|
52
|
+
for (const values of Object.values(flattenedByLocale)) {
|
|
53
|
+
for (const key of Object.keys(values)) {
|
|
54
|
+
keySet.add(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const keys = Array.from(keySet).sort((left, right) => left.localeCompare(right));
|
|
58
|
+
const units = keys
|
|
59
|
+
.map((key) => {
|
|
60
|
+
const sourceValue = flattenedByLocale[sourceLocale]?.[key] ?? "";
|
|
61
|
+
const targetValue = flattenedByLocale[targetLocale]?.[key] ?? "";
|
|
62
|
+
return [
|
|
63
|
+
` <trans-unit id="${escapeXml(key)}">`,
|
|
64
|
+
` <source>${escapeXml(sourceValue)}</source>`,
|
|
65
|
+
` <target>${escapeXml(targetValue)}</target>`,
|
|
66
|
+
" </trans-unit>",
|
|
67
|
+
].join("\n");
|
|
68
|
+
})
|
|
69
|
+
.join("\n");
|
|
70
|
+
return [
|
|
71
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
72
|
+
'<xliff version="1.2">',
|
|
73
|
+
` <file source-language="${escapeXml(sourceLocale)}" target-language="${escapeXml(targetLocale)}" datatype="plaintext" original="gloss">`,
|
|
74
|
+
" <body>",
|
|
75
|
+
units,
|
|
76
|
+
" </body>",
|
|
77
|
+
" </file>",
|
|
78
|
+
"</xliff>",
|
|
79
|
+
"",
|
|
80
|
+
].join("\n");
|
|
81
|
+
};
|
|
82
|
+
export const parseXliffTargets = (content) => {
|
|
83
|
+
const updates = {};
|
|
84
|
+
const blocks = [...collectBlocks(content, "trans-unit"), ...collectBlocks(content, "unit")];
|
|
85
|
+
for (const block of blocks) {
|
|
86
|
+
const target = readTagContent(block.content, "target");
|
|
87
|
+
const source = readTagContent(block.content, "source");
|
|
88
|
+
const value = target.length > 0 ? target : source;
|
|
89
|
+
updates[block.id] = value;
|
|
90
|
+
}
|
|
91
|
+
return updates;
|
|
92
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mxml3gend/gloss",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local-first CLI + web app for managing i18n translation files",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"dev:server": "tsx src/devServer.ts",
|
|
28
28
|
"build": "tsc -p tsconfig.json && node ./scripts/copy-ui-dist.mjs",
|
|
29
29
|
"prepublishOnly": "npm run build",
|
|
30
|
-
"test": "npm run build && node --test tests/**/*.test.mjs"
|
|
30
|
+
"test": "npm run build && node --test tests/**/*.test.mjs",
|
|
31
|
+
"test:perf": "npm run build && node --test tests/performance.regression.test.mjs"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/cors": "^2.8.19",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root{--text-primary: #0f2343;--text-secondary: #3f5478;--text-muted: #5b7196;--surface-card: #ffffff;--surface-border: #d6e0f0;font-family:Manrope,Avenir Next,Segoe UI,sans-serif;line-height:1.45;font-weight:500;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,*:before,*:after{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;background:radial-gradient(circle at 8% 0%,rgba(255,186,132,.26),transparent 42%),radial-gradient(circle at 90% 18%,rgba(120,184,255,.28),transparent 40%),linear-gradient(180deg,#f3f7ff,#f8f1e8)}#root{width:100%}.gloss-app{--space-1: .5rem;--space-2: .75rem;--space-3: 1rem;--space-4: 1.5rem;--space-5: 2rem;max-width:1360px;margin:0 auto;padding:var(--space-4) var(--space-3) calc(var(--space-5) + var(--space-2));color:var(--text-primary)}.hero{padding:var(--space-3) var(--space-4);border-radius:16px;border:1px solid #d9e3f6;background:linear-gradient(140deg,#fdfeff,#f8faff 55%,#f4f8ff);box-shadow:0 10px 24px #16264612}.hero__top{display:flex;justify-content:flex-end;align-items:center;gap:1rem;flex-wrap:wrap}.hero__eyebrow{margin:0;text-transform:uppercase;letter-spacing:.08em;font-size:.75rem;color:var(--text-muted)}.hero__title{margin:.4rem 0 0;font-size:clamp(2.1rem,5vw,3.5rem);line-height:1;letter-spacing:-.02em}.hero__logo{display:block;width:min(420px,100%);height:auto;margin:.4rem 0 0}.hero__summary{margin:var(--space-2) 0 var(--space-2);max-width:72ch;color:var(--text-secondary);line-height:1.45}.hero__stats{display:flex;flex-wrap:wrap;gap:var(--space-1);margin-bottom:var(--space-3)}.stat-chip{display:flex;align-items:baseline;gap:.45rem;padding:.4rem .68rem;border-radius:999px;background:#fffffff5;border:1px solid rgba(21,37,66,.09)}.stat-chip span{font-size:.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}.stat-chip strong{font-size:1rem}.hero__actions{display:flex;flex-wrap:wrap;gap:.625rem}.language-switch{display:inline-flex;align-items:center;gap:.45rem;padding:.32rem .4rem;border-radius:999px;border:1px solid var(--surface-border);background:#fffffff0}.support-cards{margin-top:var(--space-2);display:grid;gap:var(--space-1);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));opacity:.88}.support-card{border-radius:12px;border:1px solid #e2e9f7;background:#fbfcff;padding:var(--space-2) var(--space-3)}.support-card h2{margin:0;font-size:.95rem}.support-card p{margin:var(--space-1) 0 0;color:var(--text-secondary);font-size:.88rem;line-height:1.45}.editor-shell{margin-top:var(--space-3);border-radius:18px;border:1px solid #dbe4f5;background:#fdfefe;padding:var(--space-3);box-shadow:0 16px 30px #0f234c12;display:grid;gap:var(--space-2)}.status-bar{margin:0;border-radius:12px;border:1px solid #dbe5f7;background:#f8fbff;padding:.625rem .75rem;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);flex-wrap:wrap}.status-bar__main{margin:0;font-weight:600;font-size:.9rem}.status-bar__main--error{color:#8f2935}.status-bar__main--warning{color:#7b5b23;display:flex;align-items:center;gap:var(--space-1);flex-wrap:wrap}.status-bar__main--success{color:#1d6a42}.status-bar__main--info{color:var(--text-secondary)}.status-bar__meta{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.status-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:999px;border:1px solid #cad8f2;background:#f0f5ff;color:#29476f;font-size:.76rem;font-weight:600;padding:.18rem .5rem}.status-chip[type=button]{cursor:pointer}.status-chip--muted{background:#f6f8fc;border-color:#dde4f2;color:#4d6285}.status-chip--warning{background:#fff5de;border-color:#f4d090;color:#7a4f08}.status-chip__action{opacity:.85;font-size:.72rem;border-left:1px solid currentColor;padding-left:.4rem}.status-bar__details{width:100%;border:1px solid #e0e8f7;border-radius:10px;background:#fff;padding:.6rem .75rem}.status-bar__details strong{display:block;margin-bottom:.45rem;color:#2f4b75;font-size:.82rem}.status-bar__details p{margin:0;color:var(--text-secondary);font-size:.84rem}.status-bar__details ul{margin:0;padding-left:1rem;display:grid;gap:.3rem}.status-bar__details li{color:#35527c;font-size:.82rem;line-height:1.35}.loading-state{margin:20vh auto 0;max-width:280px;border-radius:12px;border:1px solid #d5e2fb;background:#f7faff;padding:var(--space-3);text-align:center;color:var(--text-secondary)}.notice{margin:0 0 .7rem;padding:.6rem .75rem;border-radius:10px;border:1px solid transparent}.notice--error{color:#901f1a;background:#fff3f2;border-color:#f7cdc8}.notice--success{color:#0d6837;background:#edf8f0;border-color:#b6e4c3}.notice--warning{color:#7a4f08;background:#fff5de;border-color:#f4d090}.notice--stale{color:#7a2b31;background:#fff2f4;border-color:#f4c0c9;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}.editor-tabs{display:flex;gap:var(--space-1);margin:0;padding:.25rem;border:1px solid #e2e9f6;border-radius:11px;background:#f7faff}.translations-workspace{display:grid;gap:var(--space-2)}.editor-controls{display:grid;gap:var(--space-1);padding:var(--space-1);border-radius:14px;border:1px solid #e1e8f6;background:linear-gradient(180deg,#f9fbff,#f6f9ff)}.toolbar{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-1);flex-wrap:wrap;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.toolbar__primary{flex:1 1 280px;min-width:240px}.toolbar__secondary{display:flex;align-items:flex-end;gap:.5rem;flex-wrap:wrap}.toolbar__field{display:grid;gap:.32rem;min-width:160px}.toolbar__field--search input{width:100%}.toolbar__field span{font-size:.75rem;font-weight:600;color:#5f7394;text-transform:uppercase;letter-spacing:.06em}.toolbar__toggle{display:flex;align-items:center;gap:.4rem;font-size:.88rem;padding:.42rem .6rem;border:1px solid #dbe5f7;border-radius:10px;background:#fdfefe}.toolbar__advanced{width:100%;border:1px solid #dbe5f7;border-radius:12px;background:#f8fbff;padding:.7rem;display:grid;gap:.6rem}.toolbar__advanced-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap}.toolbar__advanced-head strong{font-size:.84rem;color:#2f4b75}.toolbar__advanced-actions{display:flex;gap:.4rem;flex-wrap:wrap}.toolbar__advanced-empty{margin:0;color:var(--text-secondary);font-size:.84rem}.toolbar__git-warning{margin:0;font-size:.82rem;color:#8f3c45}.toolbar__rules{display:grid;gap:.45rem}.toolbar__rule{display:grid;grid-template-columns:minmax(140px,1fr) minmax(140px,1fr) minmax(140px,1fr) auto;gap:.45rem;align-items:end;padding:.55rem;border:1px solid #d9e4f8;border-radius:10px;background:#fff}.toolbar__sort{display:flex;align-items:end;gap:.45rem;flex-wrap:wrap;padding-top:.25rem;border-top:1px dashed #d4dff4}.add-key-form{display:flex;align-items:flex-end;gap:var(--space-1);flex-wrap:wrap;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.add-key-form input{min-width:260px;flex:1 1 280px}.add-key-form__error{margin:0;color:#8f3c45;font-size:.82rem;padding:0 .125rem}.editor-main{display:grid;grid-template-columns:minmax(230px,280px) minmax(0,1fr);gap:var(--space-3);align-items:start}.editor-content{min-width:0;border:1px solid #dfe8f6;border-radius:15px;background:#fcfdff;padding:var(--space-2);box-shadow:inset 0 1px #fff}.file-tree{border:1px solid #d8e3f4;border-radius:14px;background:linear-gradient(180deg,#f8faff,#f2f7ff);padding:var(--space-2);max-height:74vh;overflow:auto;box-shadow:inset 0 1px #fff}.file-tree__title{margin:0 0 var(--space-1);font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);position:sticky;top:-.5rem;z-index:2;background:linear-gradient(180deg,#f8faff,#f8fafff5);padding:.45rem 0 .35rem}.file-tree__all-btn,.file-tree__folder-btn,.file-tree__file-btn{width:100%;text-align:left;border:1px solid transparent;border-radius:9px;background:transparent;padding:.42rem .54rem;color:var(--text-primary);font:inherit;cursor:pointer}.file-tree__all-btn,.file-tree__file-btn{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.file-tree__folder-btn{display:inline-flex;align-items:center;gap:.38rem;color:#26456f;font-weight:600}.file-tree__caret{width:.8rem;color:#50688f}.file-tree__folder-name{overflow:hidden;text-overflow:ellipsis}.file-tree__all-btn:hover,.file-tree__folder-btn:hover,.file-tree__file-btn:hover{background:#e9f1ff}.file-tree__all-btn.is-selected,.file-tree__file-btn.is-selected{border-color:#8caadd;background:#e4edff;box-shadow:inset 3px 0 #466fcf}.file-tree__file-name{overflow:hidden;text-overflow:ellipsis}.file-tree__file-count{color:var(--text-muted);font-size:.72rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;background:#edf2fd;border:1px solid #d4e0f5;border-radius:999px;padding:.1rem .38rem}.file-tree__list{list-style:none;margin:var(--space-1) 0 0;padding:0;display:grid;gap:.1rem}.file-tree__item{margin:0}.file-tree__empty{margin:.55rem 0 0;color:var(--text-secondary);font-size:.88rem}.duplicates-panel{border:1px solid var(--surface-border);border-radius:12px;background:#f8fafe;padding:.7rem}.duplicates-panel__title{margin:0 0 .65rem;font-size:.95rem;font-weight:700}.duplicates-panel__list{display:grid;gap:.75rem}.duplicate-locale{border:1px solid #d7e2f7;border-radius:10px;background:#fff;padding:.55rem}.duplicate-locale__title{margin:0 0 .45rem;font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}.duplicate-group{border:1px solid #e3ebfb;border-radius:9px;padding:.5rem;margin-top:.45rem}.duplicate-group__top{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:.5rem;align-items:center}.duplicate-group__value{overflow-wrap:anywhere}.duplicate-group__count{font-size:.78rem;color:var(--text-muted)}.duplicate-group__keys{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.2rem;color:var(--text-secondary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.table-wrap{border-radius:12px;border:1px solid #d8e3f4;background:#fff;overflow:auto;max-height:74vh}.grid-table{width:100%;min-width:1180px;border-collapse:collapse}.grid-table th{text-align:left;padding:.78rem .72rem;border-bottom:1px solid #d9e3f5;background:#f6f9ff;position:sticky;top:0;z-index:2;color:#3a557f;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em}.grid-table td{padding:.72rem .66rem;border-bottom:1px solid #edf1f8;vertical-align:top}.grid-table tr.row-state--none>td{background:#fff7f6}.grid-table tr.row-state--partial>td{background:#fffaf2}.grid-table tr.row-state--highlighted>td{background:#edf4ff}.grid-table tr.row-state--none td.value-cell--dirty,.grid-table tr.row-state--partial td.value-cell--dirty{background:#e9f2ff}.grid-table tr.row-state--none td.value-cell--dirty-missing,.grid-table tr.row-state--partial td.value-cell--dirty-missing{background:#fff2df}.grid-table tbody tr:not(.usage-files-row):not(.virtual-spacer):hover>td{background-color:#f7fafe}.key-col{width:320px;min-width:320px;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.83rem;color:#1f3659;border-right:1px solid #d7e2f4;background-clip:padding-box}.key-col--unused{color:#7a5b2f}.key-col--file-selected{box-shadow:inset 3px 0 #4a74d9}.key-col--placeholder-warning{box-shadow:inset 0 -2px #e59f35}.usage-col{width:130px}.usage-cell{color:var(--text-secondary);white-space:nowrap}.usage-cell--unused{background:#fffaf3}.usage-toggle{border:0;background:none;padding:0;font:inherit;color:#1848a3;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.usage-tag{font-size:.74rem;font-weight:600;color:#8b6a39;text-transform:lowercase}.usage-files-row td{background:#f8fbff;padding-top:.7rem;padding-bottom:.8rem}.virtual-spacer td{border-bottom:0;padding:0}.usage-files{font-size:.82rem;color:var(--text-secondary);border:1px solid #dde6f6;border-radius:10px;background:#fff;padding:.6rem .7rem}.usage-files strong{margin-right:.45rem;color:var(--text-primary)}.usage-files-list{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.26rem;color:var(--text-primary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.key-diff-block{margin-top:.7rem;padding-top:.55rem;border-top:1px dashed #d7e3f8}.key-diff-line{overflow-wrap:anywhere}.status-col{width:176px;min-width:176px;color:#4a5f80;font-size:.84rem;line-height:1.4}.status-col__tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.25rem}.status-inline-tag{display:inline-flex;align-items:center;border-radius:999px;padding:.1rem .42rem;font-size:.7rem;font-weight:700;letter-spacing:.03em}.status-inline-tag--warning{color:#8a5a15;background:#fff4df;border:1px solid #efd2a1}.status-inline-tag--info{color:#214f98;background:#edf4ff;border:1px solid #c8daf8}.locale-col,.locale-cell{min-width:220px}.value-cell{background:transparent}.value-cell--missing{background:#fffaf4}.value-cell--dirty{background:#e9f2ff}.value-cell--dirty-missing{background:#fff3e3}.value-cell--active{box-shadow:inset 0 0 0 2px #2c5ecf57}.value-input{box-sizing:border-box;width:100%;resize:vertical;min-height:2.25rem;line-height:1.35}.value-input--dirty{border-color:#0e5fd8;box-shadow:0 0 0 2px #0e5fd814}.actions-col{width:220px;min-width:220px}.row-actions{display:flex;gap:.4rem;flex-wrap:wrap}.rename-form{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center}.inline-error{color:#9a241f;width:100%;font-size:.82rem}.empty-state{margin:1rem auto;padding:2.25rem 1.25rem;border:1px dashed #dbe4f5;border-radius:12px;background:linear-gradient(180deg,#fbfdff,#f6f9ff);text-align:center;max-width:680px;color:var(--text-secondary);line-height:1.5}.footer-actions{margin-top:var(--space-2);padding:var(--space-2) 0 0;border-top:1px solid #e2e9f6;display:flex;justify-content:flex-end;position:sticky;bottom:0;background:linear-gradient(180deg,#fdfeff00,#fdfeff 32%);z-index:3}.footer-actions .btn--primary{min-width:152px;min-height:2.5rem;font-size:.95rem}.btn{border-radius:10px;border:1px solid transparent;padding:.5rem .8rem;font-size:.9rem;font-weight:600;text-decoration:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:transform .12s ease,box-shadow .12s ease,background .12s ease}.btn:hover{transform:translateY(-.5px)}.btn:disabled{opacity:.55;cursor:not-allowed;transform:none}.btn--primary{color:#fff;background:linear-gradient(135deg,#245fdf,#1b49be);box-shadow:0 10px 18px #1f59d942}.btn--ghost{color:#1d355c;border-color:#c2d0ea;background:#f6f9ff}.btn--support{color:#fff;background:linear-gradient(135deg,#ff7849,#f14c4c);box-shadow:0 7px 16px #f14c4c3b}.btn--danger{color:#9c212a;border-color:#eec1c7;background:#fff6f7}.btn--small{padding:.4rem .6rem;font-size:.8rem}.is-active{border-color:#4d73d7;background:#eaf1ff}input,textarea,select{border-radius:9px;border:1px solid #c4d2ea;background:#fff;color:var(--text-primary);padding:.48rem .6rem;font:inherit}input:focus,textarea:focus,select:focus{outline:none;border-color:#1f5eff;box-shadow:0 0 0 3px #1f5eff24}.modal-overlay{position:fixed;inset:0;background:#0e162480;display:flex;align-items:center;justify-content:center;padding:1rem;z-index:1000}.modal-dialog{width:min(480px,100%);border-radius:14px;border:1px solid var(--surface-border);background:var(--surface-card);box-shadow:0 24px 44px #0e1f4847;padding:1rem;display:grid;gap:.75rem}.modal-dialog__message{margin:0;color:var(--text-primary);white-space:pre-line}.modal-dialog__actions{display:flex;justify-content:flex-end;gap:.5rem}@media(max-width:860px){.gloss-app{padding:var(--space-3) var(--space-2) calc(var(--space-5) + var(--space-2))}.hero{padding:var(--space-3)}.hero__top{align-items:flex-start}.editor-shell{padding:var(--space-2)}.status-bar{align-items:flex-start}.editor-controls{padding:.5rem}.toolbar__primary,.toolbar__secondary,.toolbar__field,.toolbar__field--search,.toolbar__rule{width:100%}.toolbar__rule{grid-template-columns:1fr;align-items:stretch}.toolbar__field input,.toolbar__field select,.toolbar__secondary .btn,.add-key-form input{width:100%}.editor-main{grid-template-columns:1fr}.file-tree{max-height:none}}
|