localization-mcp-server 1.0.0
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/.env.example +9 -0
- package/AGENT_GUIDE.md +556 -0
- package/AUDIT_REPORT.md +244 -0
- package/PROJECT_OVERVIEW.md +140 -0
- package/dist/api-client.d.ts +11 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +67 -0
- package/dist/api-client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-aliases.d.ts +46 -0
- package/dist/locale-aliases.d.ts.map +1 -0
- package/dist/locale-aliases.js +71 -0
- package/dist/locale-aliases.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +19 -0
- package/dist/logger.js.map +1 -0
- package/dist/permissions.d.ts +127 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +75 -0
- package/dist/permissions.js.map +1 -0
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +129 -0
- package/dist/prompts.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/diff.d.ts +3 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +166 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/environment.d.ts +3 -0
- package/dist/tools/environment.d.ts.map +1 -0
- package/dist/tools/environment.js +113 -0
- package/dist/tools/environment.js.map +1 -0
- package/dist/tools/production.d.ts +3 -0
- package/dist/tools/production.d.ts.map +1 -0
- package/dist/tools/production.js +145 -0
- package/dist/tools/production.js.map +1 -0
- package/dist/tools/project-management.d.ts +3 -0
- package/dist/tools/project-management.d.ts.map +1 -0
- package/dist/tools/project-management.js +416 -0
- package/dist/tools/project-management.js.map +1 -0
- package/dist/tools/sandbox-writes.d.ts +3 -0
- package/dist/tools/sandbox-writes.d.ts.map +1 -0
- package/dist/tools/sandbox-writes.js +260 -0
- package/dist/tools/sandbox-writes.js.map +1 -0
- package/dist/tools/snapshots.d.ts +3 -0
- package/dist/tools/snapshots.d.ts.map +1 -0
- package/dist/tools/snapshots.js +50 -0
- package/dist/tools/snapshots.js.map +1 -0
- package/dist/tools/translations.d.ts +3 -0
- package/dist/tools/translations.d.ts.map +1 -0
- package/dist/tools/translations.js +135 -0
- package/dist/tools/translations.js.map +1 -0
- package/migrate-expenses.cjs +120 -0
- package/package.json +26 -0
- package/src/api-client.ts +68 -0
- package/src/index.ts +29 -0
- package/src/logger.ts +31 -0
- package/src/permissions.ts +89 -0
- package/src/prompts.ts +159 -0
- package/src/server.ts +27 -0
- package/src/tools/diff.ts +225 -0
- package/src/tools/environment.ts +175 -0
- package/src/tools/production.ts +196 -0
- package/src/tools/project-management.ts +517 -0
- package/src/tools/sandbox-writes.ts +321 -0
- package/src/tools/snapshots.ts +68 -0
- package/src/tools/translations.ts +167 -0
- package/tsconfig.json +17 -0
package/AUDIT_REPORT.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Localization Health-Check Audit Report
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-03-28
|
|
4
|
+
**Project:** travis
|
|
5
|
+
**Namespaces:** backoffice-translations (494 keys), mobile (844 keys)
|
|
6
|
+
**Locales:** en, da-DK, nb-NO, sv, uk
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
| Severity | Issue | Count | Safe to auto-fix? |
|
|
13
|
+
|----------|-------|-------|-------------------|
|
|
14
|
+
| π΄ Critical | Keys with trailing whitespace (lookup failures) | 3 | Yes |
|
|
15
|
+
| π΄ Critical | `uk` locale registered but 0 translations | all keys | No (needs content) |
|
|
16
|
+
| π High | Duplicate-value keys with same meaning | 7 (backoffice) + 39 (mobile) | No (requires intent check) |
|
|
17
|
+
| π High | Case-duplicate key pairs | 3 (backoffice) | Risky (consumer refs unknown) |
|
|
18
|
+
| π High | Country-prefixed dynamic key pattern | 40 keys (mobile) | No (structural) |
|
|
19
|
+
| π‘ Medium | Mixed naming conventions (camel/snake/Pascal) | 10+ (backoffice) + 100+ (mobile) | No (consumer refs unknown) |
|
|
20
|
+
| π‘ Medium | da-DK / nb-NO have keys not in `en` baseline | 42 / 44 (mobile) | Investigate first |
|
|
21
|
+
| π‘ Medium | Sandbox polluted with ghost test entries | 19 keys | Yes (reset sandbox) |
|
|
22
|
+
| βΉοΈ Info | `searchLocale` without `search` is a no-op | API behavior | Document only |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## π΄ CONFIRMED PROBLEMS
|
|
27
|
+
|
|
28
|
+
### 1. Trailing whitespace in key names (`mobile`)
|
|
29
|
+
|
|
30
|
+
These three keys have a trailing space in their name. Any consumer code using the exact key string without the trailing space will get a lookup miss and fall back to the key string itself at runtime.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
'accountDeleteMessage ' (has trailing space)
|
|
34
|
+
'billable ' (has trailing space)
|
|
35
|
+
'email ' (has trailing space)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Impact:** Silent runtime failures β UI shows the raw key name instead of the translated string.
|
|
39
|
+
|
|
40
|
+
**Fix:** Safe to rename these keys in sandbox (delete old, create new without space). However, this requires verifying that the consumer code references the trimmed version (`accountDeleteMessage`, `billable`, `email`). If consumer code has the space hardcoded, renaming here breaks nothing but also fixes nothing.
|
|
41
|
+
|
|
42
|
+
**Recommendation:** Fix the keys here AND audit the consumer app for the space variant.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### 2. `uk` locale: 0 translations
|
|
47
|
+
|
|
48
|
+
The `uk` locale is registered in the project but has no content in either namespace. The public endpoint returns 404:
|
|
49
|
+
```
|
|
50
|
+
GET /translations/travis/backoffice-translations/uk β 404 Not Found
|
|
51
|
+
GET /translations/travis/mobile/uk β 404 Not Found
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Impact:** Any consumer code that falls through to `uk` locale will fail completely.
|
|
55
|
+
|
|
56
|
+
**Fix:** Either:
|
|
57
|
+
- Add Ukrainian translations (content work β not auto-fixable)
|
|
58
|
+
- Remove the `uk` locale from the project until content is ready (Admin UI)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## π HIGH SEVERITY
|
|
63
|
+
|
|
64
|
+
### 3. Case-duplicate key pairs (`backoffice-translations`)
|
|
65
|
+
|
|
66
|
+
Three pairs of keys represent the same concept but differ only by casing. **All have identical English values:**
|
|
67
|
+
|
|
68
|
+
| Pair | Value |
|
|
69
|
+
|------|-------|
|
|
70
|
+
| `logIn` / `login` | "Log in" |
|
|
71
|
+
| `logOut` / `logout` | "Log out" |
|
|
72
|
+
| `Mileage` / `mileage` | "Mileage" |
|
|
73
|
+
|
|
74
|
+
**Impact:** Consumer code is using one or both. If both are referenced, translations must be kept in sync manually β a maintenance burden and a source of inconsistency across locales.
|
|
75
|
+
|
|
76
|
+
**Fix:** Cannot auto-fix β need to know which key the consumer app references. Audit the consumer codebase, pick the canonical key (camelCase is the convention), deprecate the other. This is a **breaking change** for the consumer if done without coordination.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### 4. Semantic duplicate keys (`backoffice-translations` β 5 confirmed pairs)
|
|
81
|
+
|
|
82
|
+
Different key names, identical English values. These are almost certainly storing the same concept:
|
|
83
|
+
|
|
84
|
+
| Keys | Shared value |
|
|
85
|
+
|------|-------------|
|
|
86
|
+
| `equipment_allowance` + `equipmentAllowance` | "Equipment and materials" |
|
|
87
|
+
| `expenseType` + `netlonExpenceType` | "Expense type" |
|
|
88
|
+
| `governmentAllowance` + `governmentRate` | "Government rate" |
|
|
89
|
+
| `dkCustom` + `vismaBusiness` | "Custom" |
|
|
90
|
+
| `employeeId` + `employeeNumber` | "Employee number" |
|
|
91
|
+
|
|
92
|
+
Note: `netlonExpenceType` contains a typo ("Expence").
|
|
93
|
+
|
|
94
|
+
**Fix:** Cannot auto-fix. Requires consumer audit to confirm which key is actually used. Likely one of each pair is legacy/dead code.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### 5. Semantic duplicates (`mobile` β 39 groups, 5 most critical)
|
|
99
|
+
|
|
100
|
+
| Keys | Shared value | Risk |
|
|
101
|
+
|------|-------------|------|
|
|
102
|
+
| `approvalApproved` / `approved` / `reportStatusApproved` | "Approved" | Medium β context may differ |
|
|
103
|
+
| `approvalDeclined` / `declined` / `reportStatusDeclined` | "Declined" | Medium |
|
|
104
|
+
| `approvalPending` / `pending` / `reportStatusPending` | "Pending" | Medium |
|
|
105
|
+
| `appOptions` / `settings` | "Settings" | Low β likely different screens |
|
|
106
|
+
| `approvalDecline` / `decline` | "Decline" | Medium |
|
|
107
|
+
|
|
108
|
+
These are likely intentional duplicates for context separation, but they create a translation maintenance burden: updating "Approved" requires updating 3 separate keys. If any locale translator updates only one, the UI becomes inconsistent.
|
|
109
|
+
|
|
110
|
+
**Recommendation:** Acceptable if intentional. Document them. Consider consolidating `approvalApproved` β `approved` (action vs state).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### 6. Country-prefixed dynamic key pattern (`mobile` β 40 keys)
|
|
115
|
+
|
|
116
|
+
The `mobile` namespace has 40 keys named `{COUNTRY}-type-of-{vehicle|fuel}-{type}`:
|
|
117
|
+
- 16 `DK-*` keys
|
|
118
|
+
- 12 `NO-*` keys
|
|
119
|
+
- 12 `SE-*` keys
|
|
120
|
+
|
|
121
|
+
The consumer almost certainly builds these keys dynamically:
|
|
122
|
+
```javascript
|
|
123
|
+
// Consumer likely does something like:
|
|
124
|
+
t(`${countryCode}-type-of-vehicle-${vehicleType}`)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Why this is risky:**
|
|
128
|
+
|
|
129
|
+
1. **Country code format mismatch.** The system uses locale codes `da-DK`, `nb-NO`, `sv` β but these keys use abbreviated country codes `DK`, `NO`, `SE`. If the consumer uses the wrong format, all lookups fail silently.
|
|
130
|
+
|
|
131
|
+
2. **Unsupported dynamic key detection.** No tooling can statically detect missing keys for dynamic patterns. If a new vehicle type is added, there's no guarantee all country variants are added.
|
|
132
|
+
|
|
133
|
+
3. **Cross-country duplication.** `DK-type-of-fuel-electric`, `NO-type-of-fuel-electric`, and `SE-type-of-fuel-electric` all equal "Electric" in English. Three keys, one concept.
|
|
134
|
+
|
|
135
|
+
4. **Some DK keys contain `/`** (`DK-type-of-vehicle-bicycle/non-motorized`) which is an invalid character in this system's key naming rules. These likely fail silently or cause routing issues.
|
|
136
|
+
|
|
137
|
+
**Investigation needed:**
|
|
138
|
+
- Confirm how the consumer constructs these key names (`DK` vs `da-DK`)
|
|
139
|
+
- This is the root cause of the "problem with adding some countries" mentioned in the project
|
|
140
|
+
|
|
141
|
+
**Fix:** Not auto-fixable. Options:
|
|
142
|
+
- Keep pattern but fix the `/` in key names (safe, small)
|
|
143
|
+
- Normalize to locale codes (`da-DK-type-of-vehicle-car`) β breaking change
|
|
144
|
+
- Restructure as nested: `type-of-vehicle.car.DK` β major restructure
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## π‘ MEDIUM SEVERITY
|
|
149
|
+
|
|
150
|
+
### 7. Mixed naming conventions
|
|
151
|
+
|
|
152
|
+
**`backoffice-translations`:** Predominantly camelCase with exceptions:
|
|
153
|
+
- 4 PascalCase: `Continue`, `Mileage`, `RemoveCompanyConfirmationText`, `SoftRemoveConfirmation`
|
|
154
|
+
- 5 snake_case: `car_over_20_000km`, `company_admin`, `equipment_allowance`, `first_approver`, `second_approver`
|
|
155
|
+
- 1 dash: `re-inviteAll`
|
|
156
|
+
|
|
157
|
+
**`mobile`:** No dominant convention β approximately equal split between:
|
|
158
|
+
- camelCase (~713 keys)
|
|
159
|
+
- snake_case (~66 keys)
|
|
160
|
+
- DK/NO/SE prefixed keys (40 keys)
|
|
161
|
+
- `/problems/*` error codes (22 keys)
|
|
162
|
+
- Mixed other patterns (~103 keys)
|
|
163
|
+
|
|
164
|
+
**Impact:** Makes automated tooling and agent work unreliable β cannot predict key name format.
|
|
165
|
+
|
|
166
|
+
**Fix:** Convention enforcement going forward. Existing keys: do not rename without consumer audit.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### 8. da-DK and nb-NO have keys not in `en` baseline (`mobile`)
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
da-DK: 886 keys vs en: 844 keys β 42 extra keys in da-DK
|
|
174
|
+
nb-NO: 888 keys vs en: 844 keys β 44 extra keys in nb-NO
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The extra keys include content like `aboutPurchaseTitle`, `alreadySubscribed`, `freeTrialUsedUpScrrenTitle` (note typo: "Sccren"). These look like **iOS in-app purchase / subscription strings** that may have been loaded for DK and NO markets but never added to the English baseline.
|
|
178
|
+
|
|
179
|
+
**Impact:** These keys exist only in certain locales. Consumers relying on English as fallback may not find them, or may find them only in specific locales.
|
|
180
|
+
|
|
181
|
+
**Recommendation:** Investigate if these are intentional market-specific strings or legacy content. If legacy, clean up. If intentional, add English baseline versions.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### 9. Sandbox polluted with ghost test entries
|
|
186
|
+
|
|
187
|
+
The sandbox currently has 19 keys pending deletion, all with `productionValue: null` β test artifacts from MCP validation sessions. These keys were created in sandbox and then deleted, but they remain in the diff as pending deletions.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
backoffice-translations/mcp.e2e.test (5 locales)
|
|
191
|
+
backoffice-translations/mcp.final.test (5 locales)
|
|
192
|
+
backoffice-translations/test.key (2 locales)
|
|
193
|
+
... and 16 more
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Also 3 potentially real keys pending deletion:
|
|
197
|
+
```
|
|
198
|
+
backoffice-translations/expense.amount
|
|
199
|
+
backoffice-translations/expense.delete
|
|
200
|
+
backoffice-translations/expense.save
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
These last 3 were apparently added then deleted in sandbox. Verify if they were intentional.
|
|
204
|
+
|
|
205
|
+
**Fix:** If `expense.*` keys were intentional but accidentally deleted, recreate them. Then reset sandbox to clear the ghost entries. If not intentional, just reset sandbox.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## βΉοΈ INFORMATIONAL
|
|
210
|
+
|
|
211
|
+
### `searchLocale` without `search` is a no-op
|
|
212
|
+
|
|
213
|
+
Calling `list_translations` with `searchLocale=uk` but no `search` parameter returns all entries, not just those with `uk` values. This is a backend behavior quirk. Documented in AGENT_GUIDE.md.
|
|
214
|
+
|
|
215
|
+
### Error keys: two conflicting patterns in `mobile`
|
|
216
|
+
|
|
217
|
+
There are two different slash-based error key formats in the same namespace:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
/problems/access_token_invalid β 22 keys with LEADING slash
|
|
221
|
+
problems/bad_request β 4 keys WITHOUT leading slash
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
These cannot be the same lookup pattern. The consumer must be doing two different things to hit both. One pattern is likely wrong or legacy.
|
|
225
|
+
|
|
226
|
+
Additionally, two keys contain embedded slashes that are not error codes:
|
|
227
|
+
```
|
|
228
|
+
DK-type-of-vehicle-bicycle/non-motorized
|
|
229
|
+
DK-type-of-vehicle-short-bicycle/non-motorized
|
|
230
|
+
```
|
|
231
|
+
These are accessible via URL-encoding (`%2F`) but violate the standard key naming rules (`/^[a-zA-Z0-9._-]+$/`). They exist because they were imported via ZIP before the validation rule was in place.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Recommended fix order
|
|
236
|
+
|
|
237
|
+
1. **Fix trailing whitespace keys** (3 keys in `mobile`) β safe, small, confirmed impact
|
|
238
|
+
2. **Clarify `uk` locale** β remove or fill
|
|
239
|
+
3. **Clean sandbox ghost entries** β reset sandbox after confirming `expense.*` keys
|
|
240
|
+
4. **Investigate `/` in `DK-type-of-vehicle-bicycle/non-motorized`** keys β may cause routing failures
|
|
241
|
+
5. **Investigate da-DK/nb-NO extra keys** β likely legacy, needs product decision
|
|
242
|
+
6. **Audit consumer app for duplicate key usage** β needed before consolidating duplicates
|
|
243
|
+
|
|
244
|
+
**Do not auto-fix:** naming convention normalization, duplicate key consolidation, country-prefix pattern restructure. All require consumer codebase audit first.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Localization System β Project Overview for Agents
|
|
2
|
+
|
|
3
|
+
> Read this before starting any work. This is the authoritative reference for how this system works, what environments exist, and how to use them correctly.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What this system is
|
|
8
|
+
|
|
9
|
+
This is a **localization management backend** that replaces Locize. It stores, serves, and manages translation keys for consumer applications (currently: the TRAVIS app). It is not a translation tool for its own UI β the admin-ui is in English only and has no i18n layer.
|
|
10
|
+
|
|
11
|
+
The system has two roles:
|
|
12
|
+
1. **API server** β serves translation JSON to consumer apps at runtime
|
|
13
|
+
2. **Management backend** β agents and humans create/edit/review translation keys
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Project β Namespace β Locale hierarchy
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
project (e.g. "travis")
|
|
21
|
+
ββ namespace (e.g. "backoffice-translations", "mobile")
|
|
22
|
+
ββ key (e.g. "button.save")
|
|
23
|
+
ββ locale values (en, da-DK, nb-NO, sv, uk)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Consumers fetch translations by **project + namespace + locale**:
|
|
27
|
+
```
|
|
28
|
+
GET /translations/{projectSlug}/{namespace}/{locale}
|
|
29
|
+
β { "button.save": "Save", "button.cancel": "Cancel", ... }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This is a **public unauthenticated endpoint**. It serves production data only.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Environments: Sandbox vs Production
|
|
37
|
+
|
|
38
|
+
### Production
|
|
39
|
+
- What consumers fetch at runtime
|
|
40
|
+
- Served at: `GET /translations/{slug}/{namespace}/{locale}` (no auth)
|
|
41
|
+
- Read-only for agents β no MCP tool writes to it
|
|
42
|
+
- Changed only by: Admin UI β "Push to Production" button
|
|
43
|
+
|
|
44
|
+
### Sandbox
|
|
45
|
+
- The working copy β all agent writes go here
|
|
46
|
+
- Served at: `GET /translations/projects/{slug}/sandbox/namespaces/{ns}/entries` (auth required)
|
|
47
|
+
- Invisible to consumers until promoted
|
|
48
|
+
- Full CRUD access for agents
|
|
49
|
+
|
|
50
|
+
### Rule: agents always work in sandbox
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Dev work β Sandbox β
|
|
54
|
+
Production reads β OK β
(for review/comparison)
|
|
55
|
+
Production writes β IMPOSSIBLE via MCP (by design)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Environment β URL mapping
|
|
61
|
+
|
|
62
|
+
| Context | URL pattern | Auth | Consumer-visible? |
|
|
63
|
+
|---------|-------------|------|-------------------|
|
|
64
|
+
| Production read (consumer) | `/translations/{slug}/{ns}/{locale}` | None | β
Yes |
|
|
65
|
+
| Production read (MCP) | `/translations/projects/{slug}/namespaces/{ns}/entries` | MCP token | Read only |
|
|
66
|
+
| Sandbox read/write (MCP) | `/translations/projects/{slug}/sandbox/namespaces/{ns}/entries` | MCP token | β No |
|
|
67
|
+
| Sandbox management | `/translations/projects/{slug}/sandbox/{init,reset,diff,status}` | MCP token | β No |
|
|
68
|
+
|
|
69
|
+
**This is deterministic.** There is no implicit environment detection. The sandbox and production endpoints are different paths. An agent cannot accidentally write to production because no production-write endpoints exist in the MCP server.
|
|
70
|
+
|
|
71
|
+
### Verifying which environment you're targeting
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
# Check sandbox state
|
|
75
|
+
get_environment_status({ projectSlug: "travis" })
|
|
76
|
+
|
|
77
|
+
# Read from sandbox (default)
|
|
78
|
+
list_translations({ projectSlug: "travis", namespace: "mobile", env: "sandbox" })
|
|
79
|
+
|
|
80
|
+
# Read from production (for comparison only)
|
|
81
|
+
list_translations({ projectSlug: "travis", namespace: "mobile", env: "production" })
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## β οΈ If you detect production is being used for dev work
|
|
87
|
+
|
|
88
|
+
**Production should never be the target for development changes.** If you observe that:
|
|
89
|
+
- Keys are being created directly in production (bypassing sandbox)
|
|
90
|
+
- The consumer app is pointed to this system's production API in a dev environment
|
|
91
|
+
- Someone is using `POST/PATCH/DELETE` on `/translations/projects/*/namespaces/*/entries` (non-sandbox paths) for development
|
|
92
|
+
|
|
93
|
+
**You must:**
|
|
94
|
+
1. Stop and warn: "This is targeting production directly. Production data will be visible to all users immediately."
|
|
95
|
+
2. Suggest the correct flow: "Use the sandbox workflow β call `init_sandbox`, make changes there, review with `get_translation_diff`, and promote via Admin UI."
|
|
96
|
+
3. Do not proceed with production writes.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Auth setup
|
|
101
|
+
|
|
102
|
+
Every agent needs a personal MCP token. Tokens are `lmcp_` prefixed, stored as SHA256 hashes in the DB.
|
|
103
|
+
|
|
104
|
+
**For a new developer:**
|
|
105
|
+
1. Log in to Admin UI β API Tokens
|
|
106
|
+
2. Click "Generate token" β enter a name (e.g. `dev-laptop`)
|
|
107
|
+
3. Copy the token β **shown only once**
|
|
108
|
+
4. Add to `mcp-server/.env`:
|
|
109
|
+
```
|
|
110
|
+
BACKEND_URL=http://localhost:8080
|
|
111
|
+
MCP_TOKEN=lmcp_your_token_here
|
|
112
|
+
```
|
|
113
|
+
5. Restart Claude / reload MCP server
|
|
114
|
+
|
|
115
|
+
**Token lost or revoked?** Generate a new one via Admin UI. Old one cannot be recovered.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Deployment behavior
|
|
120
|
+
|
|
121
|
+
Translation keys and code deployments are **decoupled**:
|
|
122
|
+
|
|
123
|
+
| Change | What to do |
|
|
124
|
+
|--------|-----------|
|
|
125
|
+
| New/updated translation values only | Sandbox β Admin UI "Push to Production" β no code deploy needed |
|
|
126
|
+
| New key referenced in code | Add key to sandbox β push to production β deploy code (order matters!) |
|
|
127
|
+
| New locale added to project | Add via Admin UI β fill translations via MCP β push to production |
|
|
128
|
+
|
|
129
|
+
**Critical ordering rule:** If code references a key before it exists in production, runtime will return `undefined`. Always push the key to production before deploying code that uses it.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Known issues and limitations
|
|
134
|
+
|
|
135
|
+
| Issue | Status |
|
|
136
|
+
|-------|--------|
|
|
137
|
+
| `searchLocale` without `search` param is a no-op | Known limitation β documented |
|
|
138
|
+
| `uk` locale registered but has 0 translations | Data gap β needs content |
|
|
139
|
+
| da-DK and nb-NO in `mobile` have 42β44 keys not in `en` | Legacy content β investigate |
|
|
140
|
+
| Sandbox diff shows ghost "deleted" entries with `productionValue: null` | Cosmetic β cleanup on next promote |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class ApiError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly message: string;
|
|
4
|
+
readonly details?: unknown | undefined;
|
|
5
|
+
constructor(status: number, message: string, details?: unknown | undefined);
|
|
6
|
+
}
|
|
7
|
+
export declare function apiGet<T>(path: string, params?: Record<string, unknown>): Promise<T>;
|
|
8
|
+
export declare function apiPost<T>(path: string, data?: unknown): Promise<T>;
|
|
9
|
+
export declare function apiPatch<T>(path: string, data?: unknown): Promise<T>;
|
|
10
|
+
export declare function apiDelete(path: string): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAWA,qBAAa,QAAS,SAAQ,KAAK;aAEf,MAAM,EAAE,MAAM;aACd,OAAO,EAAE,MAAM;aACf,OAAO,CAAC,EAAE,OAAO;gBAFjB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,OAAO,YAAA;CAKpC;AAcD,wBAAsB,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO1F;AAED,wBAAsB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAOzE;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAO1E;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3D"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import axios, { AxiosError } from "axios";
|
|
2
|
+
const client = axios.create({
|
|
3
|
+
baseURL: process.env.BACKEND_URL ?? "http://localhost:8080",
|
|
4
|
+
headers: {
|
|
5
|
+
"Content-Type": "application/json",
|
|
6
|
+
Authorization: `Bearer ${process.env.MCP_TOKEN ?? ""}`,
|
|
7
|
+
},
|
|
8
|
+
timeout: 30_000,
|
|
9
|
+
});
|
|
10
|
+
export class ApiError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
message;
|
|
13
|
+
details;
|
|
14
|
+
constructor(status, message, details) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.message = message;
|
|
18
|
+
this.details = details;
|
|
19
|
+
this.name = "ApiError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function handleError(error) {
|
|
23
|
+
if (error instanceof AxiosError) {
|
|
24
|
+
const status = error.response?.status ?? 0;
|
|
25
|
+
const message = error.response?.data?.message ??
|
|
26
|
+
error.message ??
|
|
27
|
+
"Unknown API error";
|
|
28
|
+
throw new ApiError(status, String(message), error.response?.data);
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
export async function apiGet(path, params) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await client.get(path, { params });
|
|
35
|
+
return response.data;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
handleError(error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function apiPost(path, data) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await client.post(path, data);
|
|
44
|
+
return response.data;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
handleError(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function apiPatch(path, data) {
|
|
51
|
+
try {
|
|
52
|
+
const response = await client.patch(path, data);
|
|
53
|
+
return response.data;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
handleError(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function apiDelete(path) {
|
|
60
|
+
try {
|
|
61
|
+
await client.delete(path);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
handleError(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=api-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAE1C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,uBAAuB;IAC3D,OAAO,EAAE;QACP,cAAc,EAAE,kBAAkB;QAClC,aAAa,EAAE,UAAU,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,EAAE;KACvD;IACD,OAAO,EAAE,MAAM;CAChB,CAAC,CAAC;AAEH,MAAM,OAAO,QAAS,SAAQ,KAAK;IAEf;IACA;IACA;IAHlB,YACkB,MAAc,EACd,OAAe,EACf,OAAiB;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,WAAM,GAAN,MAAM,CAAQ;QACd,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAAU;QAGjC,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC,CAAC;QAC3C,MAAM,OAAO,GACV,KAAK,CAAC,QAAQ,EAAE,IAA6B,EAAE,OAAO;YACvD,KAAK,CAAC,OAAO;YACb,mBAAmB,CAAC;QACtB,MAAM,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,KAAK,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAI,IAAY,EAAE,MAAgC;IAC5E,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,GAAG,CAAI,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACvD,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,WAAW,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAI,IAAY,EAAE,IAAc;IAC3D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAAI,IAAI,EAAE,IAAI,CAAC,CAAC;QAClD,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,WAAW,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAI,IAAY,EAAE,IAAc;IAC5D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAI,IAAI,EAAE,IAAI,CAAC,CAAC;QACnD,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,WAAW,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,WAAW,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
// Load .env from the mcp-server directory (one level above dist/)
|
|
5
|
+
if (process.env.NODE_ENV !== "production") {
|
|
6
|
+
const { default: dotenv } = await import("dotenv");
|
|
7
|
+
const { fileURLToPath } = await import("url");
|
|
8
|
+
const { dirname, resolve } = await import("path");
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
dotenv.config({ path: resolve(__dirname, "..", ".env") });
|
|
11
|
+
}
|
|
12
|
+
if (!process.env.MCP_TOKEN) {
|
|
13
|
+
process.stderr.write("[localization-mcp] WARNING: MCP_TOKEN is not set. All API calls will fail with 401.\n");
|
|
14
|
+
}
|
|
15
|
+
if (!process.env.BACKEND_URL) {
|
|
16
|
+
process.stderr.write("[localization-mcp] BACKEND_URL not set, defaulting to http://localhost:3000\n");
|
|
17
|
+
}
|
|
18
|
+
const server = createServer();
|
|
19
|
+
const transport = new StdioServerTransport();
|
|
20
|
+
await server.connect(transport);
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,kEAAkE;AAClE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnD,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,uFAAuF,CACxF,CAAC;AACJ,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;IAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,+EAA+E,CAChF,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;AAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAE7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale alias resolution for MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* The server stores BCP 47 codes (nb-NO, da-DK).
|
|
5
|
+
* Consumer apps and local translation files often use shorter ISO 639-1 codes (no, da).
|
|
6
|
+
*
|
|
7
|
+
* This module normalises alias codes to canonical codes before sending to the backend,
|
|
8
|
+
* so agents and migration scripts can pass either form without breaking.
|
|
9
|
+
*
|
|
10
|
+
* ββ Alias table ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
11
|
+
* no β nb-NO (Norwegian BokmΓ₯l β most common alias)
|
|
12
|
+
* nb β nb-NO
|
|
13
|
+
* nn β nb-NO (Norwegian Nynorsk β treated as BokmΓ₯l in this system)
|
|
14
|
+
* da β da-DK (Danish)
|
|
15
|
+
* dk β da-DK (incorrect but common mistake)
|
|
16
|
+
*
|
|
17
|
+
* Codes that are already canonical (en, sv, uk, nb-NO, da-DK) pass through unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LOCALE_ALIASES: Readonly<Record<string, string>>;
|
|
20
|
+
export interface LocaleRemapping {
|
|
21
|
+
from: string;
|
|
22
|
+
to: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves locale aliases in a localeβvalue map.
|
|
26
|
+
*
|
|
27
|
+
* Returns the resolved map (with canonical locale codes as keys)
|
|
28
|
+
* and the list of remappings that were applied, so callers can
|
|
29
|
+
* inform the agent about what was normalised.
|
|
30
|
+
*
|
|
31
|
+
* If two input codes resolve to the same canonical code, the last one wins
|
|
32
|
+
* (e.g. both "no" and "nb" would map to "nb-NO").
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveLocaleAliases(values: Record<string, string>): {
|
|
35
|
+
resolved: Record<string, string>;
|
|
36
|
+
remapped: LocaleRemapping[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Resolves locale aliases across a full translations map (locale β key β value).
|
|
40
|
+
* Used by bulk_import which receives a top-level locale map.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveTranslationLocaleAliases(translations: Record<string, Record<string, string>>): {
|
|
43
|
+
resolved: Record<string, Record<string, string>>;
|
|
44
|
+
remapped: LocaleRemapping[];
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=locale-aliases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-aliases.d.ts","sourceRoot":"","sources":["../src/locale-aliases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM3D,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACpE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B,CAeA;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACnD;IACD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B,CAgBA"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale alias resolution for MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* The server stores BCP 47 codes (nb-NO, da-DK).
|
|
5
|
+
* Consumer apps and local translation files often use shorter ISO 639-1 codes (no, da).
|
|
6
|
+
*
|
|
7
|
+
* This module normalises alias codes to canonical codes before sending to the backend,
|
|
8
|
+
* so agents and migration scripts can pass either form without breaking.
|
|
9
|
+
*
|
|
10
|
+
* ββ Alias table ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
11
|
+
* no β nb-NO (Norwegian BokmΓ₯l β most common alias)
|
|
12
|
+
* nb β nb-NO
|
|
13
|
+
* nn β nb-NO (Norwegian Nynorsk β treated as BokmΓ₯l in this system)
|
|
14
|
+
* da β da-DK (Danish)
|
|
15
|
+
* dk β da-DK (incorrect but common mistake)
|
|
16
|
+
*
|
|
17
|
+
* Codes that are already canonical (en, sv, uk, nb-NO, da-DK) pass through unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export const LOCALE_ALIASES = {
|
|
20
|
+
no: "nb-NO",
|
|
21
|
+
nb: "nb-NO",
|
|
22
|
+
nn: "nb-NO",
|
|
23
|
+
da: "da-DK",
|
|
24
|
+
dk: "da-DK",
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Resolves locale aliases in a localeβvalue map.
|
|
28
|
+
*
|
|
29
|
+
* Returns the resolved map (with canonical locale codes as keys)
|
|
30
|
+
* and the list of remappings that were applied, so callers can
|
|
31
|
+
* inform the agent about what was normalised.
|
|
32
|
+
*
|
|
33
|
+
* If two input codes resolve to the same canonical code, the last one wins
|
|
34
|
+
* (e.g. both "no" and "nb" would map to "nb-NO").
|
|
35
|
+
*/
|
|
36
|
+
export function resolveLocaleAliases(values) {
|
|
37
|
+
const resolved = {};
|
|
38
|
+
const remapped = [];
|
|
39
|
+
for (const [locale, value] of Object.entries(values)) {
|
|
40
|
+
const canonical = LOCALE_ALIASES[locale.toLowerCase()];
|
|
41
|
+
if (canonical) {
|
|
42
|
+
remapped.push({ from: locale, to: canonical });
|
|
43
|
+
resolved[canonical] = value;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
resolved[locale] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { resolved, remapped };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolves locale aliases across a full translations map (locale β key β value).
|
|
53
|
+
* Used by bulk_import which receives a top-level locale map.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveTranslationLocaleAliases(translations) {
|
|
56
|
+
const resolved = {};
|
|
57
|
+
const remapped = [];
|
|
58
|
+
for (const [locale, keyMap] of Object.entries(translations)) {
|
|
59
|
+
const canonical = LOCALE_ALIASES[locale.toLowerCase()];
|
|
60
|
+
if (canonical) {
|
|
61
|
+
remapped.push({ from: locale, to: canonical });
|
|
62
|
+
// Merge into existing canonical bucket if it exists (e.g. both "no" and "nb" provided)
|
|
63
|
+
resolved[canonical] = { ...(resolved[canonical] ?? {}), ...keyMap };
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
resolved[locale] = { ...(resolved[locale] ?? {}), ...keyMap };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { resolved, remapped };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=locale-aliases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-aliases.js","sourceRoot":"","sources":["../src/locale-aliases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,CAAC,MAAM,cAAc,GAAqC;IAC9D,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;CACZ,CAAC;AAOF;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAA8B;IAIjE,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAsB,EAAE,CAAC;IAEvC,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACrD,MAAM,SAAS,GAAG,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QACvD,IAAI,SAAS,EAAE,CAAC;YACd,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;YAC/C,QAAQ,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,+BAA+B,CAC7C,YAAoD;IAKpD,MAAM,QAAQ,GAA2C,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAsB,EAAE,CAAC;IAEvC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5D,MAAM,SAAS,GAAG,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QACvD,IAAI,SAAS,EAAE,CAAC;YACd,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;YAC/C,uFAAuF;YACvF,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QACtE,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QAChE,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface AuditEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
operation: string;
|
|
4
|
+
params: Record<string, unknown>;
|
|
5
|
+
result: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function logWrite(operation: string, params: Record<string, unknown>, result: unknown): void;
|
|
8
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,QAAQ,CACtB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,OAAO,GACd,IAAI,CAaN"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const LOG_FILE = process.env.AUDIT_LOG_PATH ?? path.join(process.cwd(), "mcp-audit.log");
|
|
4
|
+
export function logWrite(operation, params, result) {
|
|
5
|
+
const entry = {
|
|
6
|
+
timestamp: new Date().toISOString(),
|
|
7
|
+
operation,
|
|
8
|
+
params,
|
|
9
|
+
result,
|
|
10
|
+
};
|
|
11
|
+
try {
|
|
12
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Log to stderr if file write fails β do not crash the MCP server
|
|
16
|
+
process.stderr.write(`[audit] Failed to write to ${LOG_FILE}: ${JSON.stringify(entry)}\n`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=logger.js.map
|