@mxml3gend/gloss 0.1.1 → 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 +74 -0
- package/dist/baseline.js +118 -0
- package/dist/cache.js +78 -0
- package/dist/cacheMetrics.js +120 -0
- package/dist/check.js +510 -0
- package/dist/config.js +214 -10
- package/dist/fs.js +105 -6
- package/dist/gitDiff.js +113 -0
- package/dist/hooks.js +101 -0
- package/dist/index.js +437 -12
- package/dist/renameKeyUsage.js +4 -12
- package/dist/server.js +163 -9
- package/dist/translationKeys.js +20 -0
- package/dist/translationTree.js +42 -0
- package/dist/typegen.js +30 -0
- package/dist/ui/Gloss_logo.png +0 -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/ui/logo_full.png +0 -0
- package/dist/usage.js +105 -22
- package/dist/usageExtractor.js +151 -0
- package/dist/usageScanner.js +110 -28
- package/dist/xliff.js +92 -0
- package/package.json +15 -5
- package/dist/ui/assets/index-CREq9Gop.css +0 -1
- package/dist/ui/assets/index-Dhb2pVPI.js +0 -10
|
@@ -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>
|
|
Binary file
|
package/dist/usage.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { extractTranslationKeys } from "./usageExtractor.js";
|
|
5
6
|
const SUPPORTED_EXTENSIONS = [
|
|
6
7
|
".ts",
|
|
7
8
|
".tsx",
|
|
@@ -33,23 +34,65 @@ 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));
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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);
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
|
-
return
|
|
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 };
|
|
53
96
|
};
|
|
54
97
|
const extractRelativeImports = (content) => {
|
|
55
98
|
const imports = new Set();
|
|
@@ -120,7 +163,7 @@ const isPageFile = (relativePath) => {
|
|
|
120
163
|
isNextAppPage ||
|
|
121
164
|
isSvelteKitPage);
|
|
122
165
|
};
|
|
123
|
-
const collectFiles = async (directory, projectDir, shouldScanFile, files) => {
|
|
166
|
+
const collectFiles = async (directory, projectDir, shouldScanFile, cfg, previousFiles, nextFiles, files) => {
|
|
124
167
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
125
168
|
for (const entry of entries) {
|
|
126
169
|
if (entry.name.startsWith(".")) {
|
|
@@ -131,7 +174,7 @@ const collectFiles = async (directory, projectDir, shouldScanFile, files) => {
|
|
|
131
174
|
if (SKIP_DIRECTORIES.has(entry.name)) {
|
|
132
175
|
continue;
|
|
133
176
|
}
|
|
134
|
-
await collectFiles(fullPath, projectDir, shouldScanFile, files);
|
|
177
|
+
await collectFiles(fullPath, projectDir, shouldScanFile, cfg, previousFiles, nextFiles, files);
|
|
135
178
|
continue;
|
|
136
179
|
}
|
|
137
180
|
if (!entry.isFile() || !isSupportedFile(entry.name)) {
|
|
@@ -144,16 +187,38 @@ const collectFiles = async (directory, projectDir, shouldScanFile, files) => {
|
|
|
144
187
|
if (!shouldScanFile(relativePath)) {
|
|
145
188
|
continue;
|
|
146
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
|
+
}
|
|
147
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
|
+
});
|
|
148
213
|
files.push({
|
|
149
214
|
filePath: fullPath,
|
|
150
215
|
relativePath,
|
|
151
|
-
keys:
|
|
152
|
-
imports
|
|
216
|
+
keys: new Set(keys),
|
|
217
|
+
imports,
|
|
153
218
|
});
|
|
154
219
|
}
|
|
155
220
|
};
|
|
156
|
-
export async function buildKeyUsageMap(cfg) {
|
|
221
|
+
export async function buildKeyUsageMap(cfg, options) {
|
|
157
222
|
const root = projectRoot();
|
|
158
223
|
const i18nDirectory = translationsDir(cfg);
|
|
159
224
|
const candidateRoots = [
|
|
@@ -164,6 +229,10 @@ export async function buildKeyUsageMap(cfg) {
|
|
|
164
229
|
path.join(root, "routes"),
|
|
165
230
|
];
|
|
166
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();
|
|
167
236
|
const files = [];
|
|
168
237
|
const shouldScanFile = createScanMatcher(cfg.scan);
|
|
169
238
|
for (const sourceRoot of sourceRoots) {
|
|
@@ -171,7 +240,21 @@ export async function buildKeyUsageMap(cfg) {
|
|
|
171
240
|
if (!stat?.isDirectory()) {
|
|
172
241
|
continue;
|
|
173
242
|
}
|
|
174
|
-
await collectFiles(sourceRoot, root, shouldScanFile, 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
|
+
}
|
|
175
258
|
}
|
|
176
259
|
const fileByPath = new Map(files.map((file) => [path.resolve(file.filePath), file]));
|
|
177
260
|
const adjacency = new Map();
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { isLikelyTranslationKey } from "./translationKeys.js";
|
|
4
|
+
const REGEX_PATTERNS = [
|
|
5
|
+
/\b(?:t|i18n\.t|translate)\(\s*["'`]([^"'`]+)["'`]\s*[\),]/g,
|
|
6
|
+
/\bi18nKey\s*=\s*["'`]([^"'`]+)["'`]/g,
|
|
7
|
+
];
|
|
8
|
+
const normalizeMode = (mode) => mode === "ast" ? "ast" : "regex";
|
|
9
|
+
const scriptKindForFile = (filePath) => {
|
|
10
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
11
|
+
if (extension === ".tsx") {
|
|
12
|
+
return ts.ScriptKind.TSX;
|
|
13
|
+
}
|
|
14
|
+
if (extension === ".jsx") {
|
|
15
|
+
return ts.ScriptKind.JSX;
|
|
16
|
+
}
|
|
17
|
+
if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
|
|
18
|
+
return ts.ScriptKind.TS;
|
|
19
|
+
}
|
|
20
|
+
return ts.ScriptKind.JS;
|
|
21
|
+
};
|
|
22
|
+
const getLiteralText = (node) => {
|
|
23
|
+
if (!node) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
27
|
+
return node.text;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
const isTranslationCallee = (expression) => {
|
|
32
|
+
if (ts.isIdentifier(expression)) {
|
|
33
|
+
return expression.text === "t" || expression.text === "translate";
|
|
34
|
+
}
|
|
35
|
+
if (ts.isPropertyAccessExpression(expression) &&
|
|
36
|
+
ts.isIdentifier(expression.expression)) {
|
|
37
|
+
return expression.expression.text === "i18n" && expression.name.text === "t";
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
const isI18nKeyAttribute = (name) => ts.isIdentifier(name) && name.text === "i18nKey";
|
|
42
|
+
const extractWithAst = (source, filePath) => {
|
|
43
|
+
const keys = [];
|
|
44
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, scriptKindForFile(filePath));
|
|
45
|
+
const pushKey = (value) => {
|
|
46
|
+
const key = value?.trim();
|
|
47
|
+
if (key && isLikelyTranslationKey(key)) {
|
|
48
|
+
keys.push(key);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const visit = (node) => {
|
|
52
|
+
if (ts.isCallExpression(node) && isTranslationCallee(node.expression)) {
|
|
53
|
+
pushKey(getLiteralText(node.arguments[0]));
|
|
54
|
+
}
|
|
55
|
+
else if (ts.isJsxAttribute(node) && isI18nKeyAttribute(node.name)) {
|
|
56
|
+
if (node.initializer && ts.isStringLiteral(node.initializer)) {
|
|
57
|
+
pushKey(node.initializer.text);
|
|
58
|
+
}
|
|
59
|
+
else if (node.initializer &&
|
|
60
|
+
ts.isJsxExpression(node.initializer) &&
|
|
61
|
+
node.initializer.expression) {
|
|
62
|
+
pushKey(getLiteralText(node.initializer.expression));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
ts.forEachChild(node, visit);
|
|
66
|
+
};
|
|
67
|
+
visit(sourceFile);
|
|
68
|
+
return keys;
|
|
69
|
+
};
|
|
70
|
+
const extractWithRegex = (source) => {
|
|
71
|
+
const keys = [];
|
|
72
|
+
for (const regex of REGEX_PATTERNS) {
|
|
73
|
+
let match = regex.exec(source);
|
|
74
|
+
while (match) {
|
|
75
|
+
const key = match[1]?.trim();
|
|
76
|
+
if (key && isLikelyTranslationKey(key)) {
|
|
77
|
+
keys.push(key);
|
|
78
|
+
}
|
|
79
|
+
match = regex.exec(source);
|
|
80
|
+
}
|
|
81
|
+
regex.lastIndex = 0;
|
|
82
|
+
}
|
|
83
|
+
return keys;
|
|
84
|
+
};
|
|
85
|
+
export const extractTranslationKeys = (source, filePath, mode) => {
|
|
86
|
+
const normalizedMode = normalizeMode(mode);
|
|
87
|
+
if (normalizedMode === "ast") {
|
|
88
|
+
return extractWithAst(source, filePath);
|
|
89
|
+
}
|
|
90
|
+
return extractWithRegex(source);
|
|
91
|
+
};
|
|
92
|
+
export const replaceTranslationKeyLiterals = (source, filePath, oldKey, newKey, mode) => {
|
|
93
|
+
const normalizedMode = normalizeMode(mode);
|
|
94
|
+
if (normalizedMode === "regex") {
|
|
95
|
+
let replacements = 0;
|
|
96
|
+
const regex = /(\b(?:t|translate)\s*\(\s*)(['"])([^'"]+)\2/g;
|
|
97
|
+
const updated = source.replace(regex, (match, prefix, quote, key) => {
|
|
98
|
+
if (key !== oldKey) {
|
|
99
|
+
return match;
|
|
100
|
+
}
|
|
101
|
+
replacements += 1;
|
|
102
|
+
return `${prefix}${quote}${newKey}${quote}`;
|
|
103
|
+
});
|
|
104
|
+
regex.lastIndex = 0;
|
|
105
|
+
return { updated, replacements };
|
|
106
|
+
}
|
|
107
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, scriptKindForFile(filePath));
|
|
108
|
+
const edits = [];
|
|
109
|
+
const queueEditForLiteral = (node) => {
|
|
110
|
+
const start = node.getStart(sourceFile);
|
|
111
|
+
const end = node.getEnd();
|
|
112
|
+
const quote = source[start] === "`" ? "`" : source[start] === "'" ? "'" : '"';
|
|
113
|
+
edits.push({ start, end, text: `${quote}${newKey}${quote}` });
|
|
114
|
+
};
|
|
115
|
+
const visit = (node) => {
|
|
116
|
+
if (ts.isCallExpression(node) && isTranslationCallee(node.expression)) {
|
|
117
|
+
const firstArg = node.arguments[0];
|
|
118
|
+
if (firstArg &&
|
|
119
|
+
(ts.isStringLiteral(firstArg) || ts.isNoSubstitutionTemplateLiteral(firstArg)) &&
|
|
120
|
+
firstArg.text === oldKey) {
|
|
121
|
+
queueEditForLiteral(firstArg);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (ts.isJsxAttribute(node) && isI18nKeyAttribute(node.name)) {
|
|
125
|
+
if (node.initializer && ts.isStringLiteral(node.initializer)) {
|
|
126
|
+
if (node.initializer.text === oldKey) {
|
|
127
|
+
queueEditForLiteral(node.initializer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (node.initializer &&
|
|
131
|
+
ts.isJsxExpression(node.initializer) &&
|
|
132
|
+
node.initializer.expression &&
|
|
133
|
+
(ts.isStringLiteral(node.initializer.expression) ||
|
|
134
|
+
ts.isNoSubstitutionTemplateLiteral(node.initializer.expression)) &&
|
|
135
|
+
node.initializer.expression.text === oldKey) {
|
|
136
|
+
queueEditForLiteral(node.initializer.expression);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
ts.forEachChild(node, visit);
|
|
140
|
+
};
|
|
141
|
+
visit(sourceFile);
|
|
142
|
+
edits.sort((left, right) => right.start - left.start);
|
|
143
|
+
let updated = source;
|
|
144
|
+
for (const edit of edits) {
|
|
145
|
+
updated = `${updated.slice(0, edit.start)}${edit.text}${updated.slice(edit.end)}`;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
updated,
|
|
149
|
+
replacements: edits.length,
|
|
150
|
+
};
|
|
151
|
+
};
|
package/dist/usageScanner.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { extractTranslationKeys } from "./usageExtractor.js";
|
|
5
6
|
const IGNORED_DIRECTORIES = new Set([
|
|
6
7
|
"node_modules",
|
|
7
8
|
"dist",
|
|
@@ -15,12 +16,9 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
15
16
|
"storybook-static",
|
|
16
17
|
]);
|
|
17
18
|
const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
18
|
-
const USAGE_REGEXES = [
|
|
19
|
-
/\b(?:t|i18n\.t|translate)\(\s*["'`]([^"'`]+)["'`]\s*[\),]/g,
|
|
20
|
-
/\bi18nKey\s*=\s*["'`]([^"'`]+)["'`]/g,
|
|
21
|
-
];
|
|
22
19
|
const projectRoot = () => process.env.INIT_CWD || process.cwd();
|
|
23
20
|
const normalizePath = (filePath) => filePath.split(path.sep).join("/");
|
|
21
|
+
const usageScannerCache = new Map();
|
|
24
22
|
const isScannableFile = (fileName) => SCANNED_EXTENSIONS.has(path.extname(fileName));
|
|
25
23
|
const hasIgnoredPathSegment = (relativePath) => normalizePath(relativePath)
|
|
26
24
|
.split("/")
|
|
@@ -46,9 +44,69 @@ export const inferUsageRoot = (cfg) => {
|
|
|
46
44
|
}
|
|
47
45
|
return parentDirectory;
|
|
48
46
|
};
|
|
49
|
-
export
|
|
50
|
-
const
|
|
51
|
-
|
|
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();
|
|
52
110
|
const shouldScanFile = createScanMatcher(scan);
|
|
53
111
|
const scanDirectory = async (directory) => {
|
|
54
112
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
@@ -71,30 +129,54 @@ export async function scanUsage(rootDir = projectRoot(), scan) {
|
|
|
71
129
|
if (!shouldScanFile(relativePath)) {
|
|
72
130
|
continue;
|
|
73
131
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!usage[key]) {
|
|
81
|
-
usage[key] = { count: 0, files: [] };
|
|
82
|
-
seenFilesByKey.set(key, new Set());
|
|
83
|
-
}
|
|
84
|
-
usage[key].count += 1;
|
|
85
|
-
const fileSet = seenFilesByKey.get(key);
|
|
86
|
-
if (fileSet && !fileSet.has(relativePath)) {
|
|
87
|
-
fileSet.add(relativePath);
|
|
88
|
-
usage[key].files.push(relativePath);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
match = usageRegex.exec(source);
|
|
92
|
-
}
|
|
93
|
-
usageRegex.lastIndex = 0;
|
|
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;
|
|
94
138
|
}
|
|
139
|
+
const source = await fs.readFile(fullPath, "utf8");
|
|
140
|
+
const keys = extractTranslationKeys(source, fullPath, scan?.mode);
|
|
141
|
+
nextFiles.set(relativePath, {
|
|
142
|
+
signature,
|
|
143
|
+
keys,
|
|
144
|
+
mtimeMs: stat.mtimeMs,
|
|
145
|
+
sizeBytes: stat.size,
|
|
146
|
+
});
|
|
95
147
|
}
|
|
96
148
|
};
|
|
97
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
|
+
}
|
|
98
180
|
for (const value of Object.values(usage)) {
|
|
99
181
|
value.files.sort((left, right) => left.localeCompare(right));
|
|
100
182
|
}
|