Skip to content

Commit dceb0a6

Browse files
strickvlclaude
andcommitted
refactor: simplify --i18n flag to file-only approach
Replace dual JSON/file path functionality with file-only approach for cleaner, more predictable behavior. Since this is a new feature that hasn't been released yet, this change eliminates potential user confusion without breaking existing workflows. Changes: - Modify loadCustomStrings() to only accept file paths - Enhanced error handling with specific messages for file vs JSON errors - Remove inline JSON parsing from CLI argument handling - Update flag description to reflect file-only approach - Add comprehensive test suite covering all error scenarios - Update documentation to show only file-based examples Benefits: - Eliminates semantic ambiguity about argument format - Clearer error messages (file not found vs invalid JSON) - Follows Unix conventions for configuration files - Simpler implementation with better TypeScript typing - Can enhance with inline JSON later if demand exists 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ca12a25 commit dceb0a6

File tree

5 files changed

+120
-33
lines changed

5 files changed

+120
-33
lines changed

docs/guide.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -461,22 +461,29 @@ through the proxy without authentication, use `--skip-auth-preflight`.
461461

462462
## Internationalization and customization
463463

464-
code-server allows you to provide a language file or JSON to configure certain strings. This can be used for both internationalization and customization.
464+
code-server allows you to provide a JSON file to configure certain strings. This can be used for both internationalization and customization.
465465

466-
For example:
466+
Create a JSON file with your custom strings:
467+
468+
```json
469+
{
470+
"WELCOME": "Welcome to {{app}}",
471+
"LOGIN_TITLE": "{{app}} Access Portal",
472+
"LOGIN_BELOW": "Please log in to continue",
473+
"PASSWORD_PLACEHOLDER": "Enter Password"
474+
}
475+
```
476+
477+
Then reference the file:
467478

468479
```shell
469-
code-server --i18n /custom-strings.json
470-
code-server --i18n '{"WELCOME": "{{app}} ログイン"}'
480+
code-server --i18n /path/to/custom-strings.json
471481
```
472482

473483
Or this can be done in the config file:
474484

475485
```yaml
476-
i18n: |
477-
{
478-
"WELCOME": "Welcome to the {{app}} Development Portal"
479-
}
486+
i18n: /path/to/custom-strings.json
480487
```
481488
482489
You can combine this with the `--locale` flag to configure language support for both code-server and VS Code in cases where code-server has no support but VS Code does. If you are using this for internationalization, please consider sending us a pull request to contribute it to `src/node/i18n/locales`.

src/node/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ export const options: Options<Required<UserProvidedArgs>> = {
299299
},
300300
i18n: {
301301
type: "string",
302-
description: "Path to JSON file or raw JSON string with custom translations. Merges with default strings and supports all i18n keys.",
302+
description: "Path to JSON file with custom translations. Merges with default strings and supports all i18n keys.",
303303
},
304304
}
305305

src/node/i18n/index.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,24 @@ const defaultResources = {
2525
}
2626

2727

28-
export async function loadCustomStrings(customStringsArg: string): Promise<void> {
29-
28+
export async function loadCustomStrings(filePath: string): Promise<void> {
3029
try {
31-
let customStringsData: Record<string, any>
32-
33-
// Try to parse as JSON first
34-
try {
35-
customStringsData = JSON.parse(customStringsArg)
36-
} catch {
37-
// If JSON parsing fails, treat as file path
38-
const fileContent = await fs.readFile(customStringsArg, "utf8")
39-
customStringsData = JSON.parse(fileContent)
40-
}
30+
// Read custom strings from file path only
31+
const fileContent = await fs.readFile(filePath, "utf8")
32+
const customStringsData = JSON.parse(fileContent)
4133

4234
// User-provided strings override all languages.
4335
Object.keys(defaultResources).forEach((locale) => {
4436
i18next.addResourceBundle(locale, "translation", customStringsData)
4537
})
4638
} catch (error) {
47-
throw new Error(`Failed to load custom strings: ${error instanceof Error ? error.message : String(error)}`)
39+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
40+
throw new Error(`Custom strings file not found: ${filePath}\nPlease ensure the file exists and is readable.`)
41+
} else if (error instanceof SyntaxError) {
42+
throw new Error(`Invalid JSON in custom strings file: ${filePath}\n${error.message}`)
43+
} else {
44+
throw new Error(`Failed to load custom strings from ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
45+
}
4846
}
4947
}
5048

test/unit/node/cli.test.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -349,21 +349,18 @@ describe("parser", () => {
349349
})
350350
})
351351

352-
it("should parse i18n flag", async () => {
353-
// Test with JSON string
354-
const jsonString = '{"WELCOME": "Custom Welcome", "LOGIN_TITLE": "My App"}'
355-
const args = parse(["--i18n", jsonString])
352+
it("should parse i18n flag with file path", async () => {
353+
// Test with file path (no validation at CLI parsing level)
354+
const args = parse(["--i18n", "/path/to/custom-strings.json"])
356355
expect(args).toEqual({
357-
i18n: jsonString,
356+
i18n: "/path/to/custom-strings.json",
358357
})
359358
})
360359

361-
it("should parse i18n file paths and JSON", async () => {
362-
// Test with valid JSON that looks like a file path
363-
expect(() => parse(["--i18n", "/path/to/file.json"])).not.toThrow()
364-
365-
// Test with JSON string (no validation at CLI level)
366-
expect(() => parse(["--i18n", '{"valid": "json"}'])).not.toThrow()
360+
it("should parse i18n flag with relative file path", async () => {
361+
// Test with relative file path
362+
expect(() => parse(["--i18n", "./custom-strings.json"])).not.toThrow()
363+
expect(() => parse(["--i18n", "strings.json"])).not.toThrow()
367364
})
368365

369366
it("should support app-name and deprecated welcome-text flags", async () => {

test/unit/node/i18n.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { promises as fs } from "fs"
2+
import * as os from "os"
3+
import * as path from "path"
4+
import { loadCustomStrings } from "../../../src/node/i18n"
5+
6+
describe("i18n", () => {
7+
let tempDir: string
8+
let validJsonFile: string
9+
let invalidJsonFile: string
10+
let nonExistentFile: string
11+
12+
beforeEach(async () => {
13+
// Create temporary directory for test files
14+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "code-server-i18n-test-"))
15+
16+
// Create test files
17+
validJsonFile = path.join(tempDir, "valid.json")
18+
invalidJsonFile = path.join(tempDir, "invalid.json")
19+
nonExistentFile = path.join(tempDir, "does-not-exist.json")
20+
21+
// Write valid JSON file
22+
await fs.writeFile(validJsonFile, JSON.stringify({
23+
"WELCOME": "Custom Welcome",
24+
"LOGIN_TITLE": "My Custom App",
25+
"LOGIN_BELOW": "Please log in to continue"
26+
}))
27+
28+
// Write invalid JSON file
29+
await fs.writeFile(invalidJsonFile, '{"WELCOME": "Missing closing quote}')
30+
})
31+
32+
afterEach(async () => {
33+
// Clean up temporary directory
34+
await fs.rmdir(tempDir, { recursive: true })
35+
})
36+
37+
describe("loadCustomStrings", () => {
38+
it("should load valid JSON file successfully", async () => {
39+
// Should not throw an error
40+
await expect(loadCustomStrings(validJsonFile)).resolves.toBeUndefined()
41+
})
42+
43+
it("should throw clear error for non-existent file", async () => {
44+
await expect(loadCustomStrings(nonExistentFile)).rejects.toThrow(
45+
`Custom strings file not found: ${nonExistentFile}\nPlease ensure the file exists and is readable.`
46+
)
47+
})
48+
49+
it("should throw clear error for invalid JSON", async () => {
50+
await expect(loadCustomStrings(invalidJsonFile)).rejects.toThrow(
51+
`Invalid JSON in custom strings file: ${invalidJsonFile}`
52+
)
53+
})
54+
55+
it("should handle empty JSON object", async () => {
56+
const emptyJsonFile = path.join(tempDir, "empty.json")
57+
await fs.writeFile(emptyJsonFile, "{}")
58+
59+
await expect(loadCustomStrings(emptyJsonFile)).resolves.toBeUndefined()
60+
})
61+
62+
it("should handle nested JSON objects", async () => {
63+
const nestedJsonFile = path.join(tempDir, "nested.json")
64+
await fs.writeFile(nestedJsonFile, JSON.stringify({
65+
"WELCOME": "Hello World",
66+
"NESTED": {
67+
"KEY": "Value"
68+
}
69+
}))
70+
71+
await expect(loadCustomStrings(nestedJsonFile)).resolves.toBeUndefined()
72+
})
73+
74+
it("should handle special characters and unicode", async () => {
75+
const unicodeJsonFile = path.join(tempDir, "unicode.json")
76+
await fs.writeFile(unicodeJsonFile, JSON.stringify({
77+
"WELCOME": "欢迎来到 code-server",
78+
"LOGIN_TITLE": "Willkommen bei {{app}}",
79+
"SPECIAL": "Special chars: àáâãäåæçèéêë 🚀 ♠️ ∆"
80+
}), "utf8")
81+
82+
await expect(loadCustomStrings(unicodeJsonFile)).resolves.toBeUndefined()
83+
})
84+
})
85+
})

0 commit comments

Comments
 (0)