@drone1/alt 0.9.3 → 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.
Files changed (97) hide show
  1. package/.github/workflows/test.yml +30 -0
  2. package/.mocharc.cjs +13 -0
  3. package/README.md +8 -15
  4. package/localization/.localization.cache.json +1244 -140
  5. package/localization/aa.json +9 -1
  6. package/localization/af.json +9 -1
  7. package/localization/agq.json +9 -1
  8. package/localization/ak.json +9 -1
  9. package/localization/am.json +9 -1
  10. package/localization/ar.json +9 -1
  11. package/localization/as.json +9 -1
  12. package/localization/asa.json +9 -1
  13. package/localization/ast.json +9 -1
  14. package/localization/az.json +9 -1
  15. package/localization/ba.json +9 -1
  16. package/localization/bas.json +9 -1
  17. package/localization/be.json +9 -1
  18. package/localization/bem.json +9 -1
  19. package/localization/bez.json +9 -1
  20. package/localization/bg.json +9 -1
  21. package/localization/bm.json +9 -1
  22. package/localization/bn.json +9 -1
  23. package/localization/bo.json +9 -1
  24. package/localization/br.json +9 -1
  25. package/localization/brx.json +9 -1
  26. package/localization/bs.json +9 -1
  27. package/localization/byn.json +9 -1
  28. package/localization/ca.json +9 -1
  29. package/localization/ccp.json +9 -1
  30. package/localization/cd-RU.json +9 -1
  31. package/localization/ceb.json +9 -1
  32. package/localization/cgg.json +9 -1
  33. package/localization/chr.json +9 -1
  34. package/localization/co.json +9 -1
  35. package/localization/cs.json +9 -1
  36. package/localization/cu-RU.json +9 -1
  37. package/localization/da.json +9 -1
  38. package/localization/de-AT.json +9 -1
  39. package/localization/de-CH.json +9 -1
  40. package/localization/de-DE.json +9 -1
  41. package/localization/dua.json +9 -1
  42. package/localization/dv.json +9 -1
  43. package/localization/dz.json +9 -1
  44. package/localization/ebu.json +9 -1
  45. package/localization/en.json +9 -1
  46. package/localization/es-ES.json +9 -1
  47. package/localization/es-MX.json +9 -1
  48. package/localization/et.json +9 -1
  49. package/localization/eu.json +9 -1
  50. package/localization/fr-CA.json +9 -1
  51. package/localization/fr-CH.json +9 -1
  52. package/localization/fr-FR.json +9 -1
  53. package/localization/gsw.json +9 -1
  54. package/localization/hi.json +9 -1
  55. package/localization/hr.json +9 -1
  56. package/localization/hy.json +9 -1
  57. package/localization/ja.json +9 -1
  58. package/localization/km.json +9 -1
  59. package/localization/ksf.json +9 -1
  60. package/localization/ku.json +9 -1
  61. package/localization/kw.json +9 -1
  62. package/localization/my.json +9 -1
  63. package/localization/nl.json +9 -1
  64. package/localization/prs.json +9 -1
  65. package/localization/reference.js +10 -1
  66. package/localization/ru.json +9 -1
  67. package/localization/sq.json +9 -1
  68. package/localization/swc.json +9 -1
  69. package/localization/th.json +9 -1
  70. package/localization/tzm-Latn-.json +9 -1
  71. package/localization/uk.json +9 -1
  72. package/localization/vi.json +9 -1
  73. package/localization/zh-Hans.json +9 -1
  74. package/localization/zh-Hant.json +9 -1
  75. package/npm-shrinkwrap.json +6498 -676
  76. package/package.json +19 -1
  77. package/src/commands/list-models.js +1 -0
  78. package/src/commands/translate.js +42 -9
  79. package/src/lib/config.js +1 -1
  80. package/src/lib/context-keys.js +12 -3
  81. package/src/lib/io.js +10 -3
  82. package/src/lib/reference-loader.js +1 -0
  83. package/src/main.mjs +2 -2
  84. package/test/README.md +37 -0
  85. package/test/common.mjs +29 -0
  86. package/test/config.test.js +78 -0
  87. package/test/fixtures/reference.js +5 -0
  88. package/test/fixtures/reference.json +5 -0
  89. package/test/fixtures/reference.jsonc +8 -0
  90. package/test/list-models.test.js +27 -0
  91. package/test/localization.test.js +124 -0
  92. package/test/main-cli.test.js +79 -0
  93. package/test/mocha.setup.js +10 -0
  94. package/test/translate-command.test.js +122 -0
  95. package/test/reference.js +0 -28
  96. package/test/reference.json +0 -28
  97. package/test/reference.jsonc +0 -29
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@drone1/alt",
4
- "version": "0.9.3",
4
+ "version": "1.0.0",
5
5
  "description": "An AI-powered localization tool",
6
6
  "main": "src/index.mjs",
7
7
  "bin": {
8
8
  "alt": "./alt.mjs"
9
9
  },
10
10
  "scripts": {
11
+ "test": "ALT_TEST=1 mocha",
12
+ "test:targeted": "ALT_TEST=1 mocha --grep 'multiple target'",
13
+ "test": "ALT_TEST=1 mocha",
14
+ "test:coverage": "ALT_TEST=1 nyc mocha",
11
15
  "localize-display-strings": "./alt.mjs translate --reference-file localization/reference.js",
12
16
  "print-all-help": "rm -f help.txt && (./alt.mjs help && echo -e '\n---\n' && ./alt.mjs help translate && echo -e '\n---\n' && ./alt.mjs help list-models) > help.txt",
13
17
  "generate-toc": "./scripts/gh-md-toc --insert README.md && rm -f README.md.orig.* README.md.toc.* && echo '\n**README.md updated with new table of contents**'"
@@ -46,5 +50,19 @@
46
50
  },
47
51
  "engines": {
48
52
  "node": ">=14.0.0"
53
+ },
54
+ "nyc": {
55
+ "check-coverage": true,
56
+ "per-file": true,
57
+ "lines": 80,
58
+ "statements": 80,
59
+ "functions": 80,
60
+ "branches": 80
61
+ },
62
+ "devDependencies": {
63
+ "chai": "^5.2.0",
64
+ "execa": "^9.5.2",
65
+ "mocha": "^11.1.0",
66
+ "nyc": "^17.1.0"
49
67
  }
50
68
  }
@@ -2,5 +2,6 @@ import { loadTranslationProvider } from '../lib/provider.js'
2
2
 
3
3
  export async function runListModels({ appState, options, log }) {
4
4
  const { apiKey, api } = await loadTranslationProvider({ __dirname: appState.__dirname, providerName: options.provider, log })
5
+ log.I(`Available models:\n`)
5
6
  return log.I(await api.listModels(apiKey))
6
7
  }
@@ -35,21 +35,41 @@ export async function runTranslation({ appState, options, log }) {
35
35
  // Validate provider
36
36
  const providerName = (options.provider ?? config.provider)?.toLowerCase()
37
37
  if (!VALID_TRANSLATION_PROVIDERS.includes(providerName)) {
38
- log.E(`Error: Unknown provider "${providerName}". Supported providers: ${VALID_TRANSLATION_PROVIDERS.join(', ')}`)
39
- process.exit(2)
38
+ throw new Error(
39
+ (providerName
40
+ ? localizeFormatted({
41
+ token: 'error-unknown-provider',
42
+ data: { providerName },
43
+ lang: appState.lang,
44
+ log
45
+ })
46
+ : localize({
47
+ token: 'error-no-provider-specified',
48
+ lang: appState.lang,
49
+ log
50
+ }))
51
+ + localizeFormatted({
52
+ token: 'supported-providers',
53
+ data: { providers: VALID_TRANSLATION_PROVIDERS.join(', ') },
54
+ lang: appState.lang,
55
+ log
56
+ })
57
+ )
40
58
  }
41
59
 
42
60
  const referenceLanguage = options.referenceLanguage || config.referenceLanguage
43
61
  if (!referenceLanguage || !referenceLanguage.length) {
44
- log.E(`Error: No reference language specified. Use --reference-language option or add 'referenceLanguages' to your config file`)
45
- process.exit(2)
62
+ throw new Error(
63
+ localize({ token: 'error-no-reference-language', lang: appState.lang, log })
64
+ )
46
65
  }
47
66
 
48
67
  // Get target languages from CLI or config
49
68
  const targetLanguages = options.targetLanguages || config.targetLanguages
50
69
  if (!targetLanguages || !targetLanguages.length) {
51
- log.E(`Error: No target languages specified. Use --target-languages option or add 'targetLanguages' to your config file`)
52
- process.exit(2)
70
+ throw new Error(
71
+ localize({ token: 'error-no-target-languages', lang: appState.lang, log })
72
+ )
53
73
  }
54
74
 
55
75
  const normalizeOutputFilenames = options.normalizeOutputFilenames || config.normalizeOutputFilenames
@@ -71,8 +91,17 @@ export async function runTranslation({ appState, options, log }) {
71
91
  // Copy to a temp location first so we can ensure it has an .mjs extension
72
92
  const referenceData = await loadReferenceFile({ appLang: appState.lang, options, tmpDir, log })
73
93
  if (!referenceData) {
74
- log.E(`No reference data found in variable "${options.referenceExportedVarName}" in ${options.referenceFile}`)
75
- process.exit(2)
94
+ throw new Error(
95
+ localizeFormatted({
96
+ token: 'error-no-reference-data-in-variable',
97
+ data: {
98
+ referenceExportedVarName: options.referenceExportedVarName,
99
+ referenceFile: options.referenceFile
100
+ },
101
+ lang: appState.lang,
102
+ log
103
+ })
104
+ )
76
105
  }
77
106
 
78
107
  const referenceHash = calculateHash(await readFileAsText(options.referenceFile))
@@ -94,6 +123,8 @@ export async function runTranslation({ appState, options, log }) {
94
123
  const { apiKey, api: translationProvider } = await loadTranslationProvider({ __dirname: appState.__dirname, providerName, log })
95
124
  log.V(`translation provider "${providerName}" loaded`)
96
125
 
126
+ log.D(`options.lookForContextData=${options.lookForContextData}`)
127
+ log.D(`config.lookForContextData=${config.lookForContextData}`)
97
128
  const addContextToTranslation = options.lookForContextData || config.lookForContextData
98
129
 
99
130
  const workQueue = []
@@ -132,9 +163,11 @@ export async function runTranslation({ appState, options, log }) {
132
163
  if (addContextToTranslation) {
133
164
  keysToProcess = keysToProcess
134
165
  .filter(key => !isContextKey({
166
+ appLang: appState.lang,
135
167
  key,
136
168
  contextPrefix,
137
- contextSuffix
169
+ contextSuffix,
170
+ log
138
171
  }))
139
172
  }
140
173
 
package/src/lib/config.js CHANGED
@@ -16,7 +16,7 @@ export async function loadConfig({ configFile, refFileDir, log }) {
16
16
  return await readJsonFile(configFilePath) || {
17
17
  provider: null,
18
18
  targetLanguages: [],
19
- lookForContextData: true,
19
+ lookForContextData: false,
20
20
  contextPrefix: '',
21
21
  contextSuffix: '',
22
22
  referenceLanguage: null,
@@ -1,10 +1,19 @@
1
- export function isContextKey({ key, contextPrefix, contextSuffix }) {
1
+ import { localizeFormatted } from '../localizer/localize.js'
2
+
3
+ export function isContextKey({ appLang, key, contextPrefix, contextSuffix, log }) {
2
4
  if (contextPrefix?.length) return key.startsWith(contextPrefix)
3
5
  if (contextSuffix?.length) return key.endsWith(contextSuffix)
4
- throw new Error(`Either the context prefix or context suffix must be defined`)
6
+ log.D(`contextPrefix=${contextPrefix}`)
7
+ log.D(`contextSuffix=${contextSuffix}`)
8
+ throw new Error(
9
+ localizeFormatted({
10
+ token: 'error-context-prefix-and-suffix-not-defined',
11
+ lang: appLang,
12
+ log
13
+ })
14
+ )
5
15
  }
6
16
 
7
17
  export function formatContextKeyFromKey({ key, prefix, suffix }) {
8
18
  return `${prefix}${key}${suffix}`
9
19
  }
10
-
package/src/lib/io.js CHANGED
@@ -7,6 +7,7 @@ import { ensureExtension, normalizeKey } from './utils.js'
7
7
  import { assertIsObj, assertValidPath } from './assert.js'
8
8
  import { pathToFileURL } from 'url'
9
9
  import { CWD } from './consts.js'
10
+ import { localizeFormatted } from '../localizer/localize.js'
10
11
 
11
12
  export async function mkTmpDir() {
12
13
  return await fsp.mkdtemp(path.join(os.tmpdir(), 'alt-'))
@@ -57,15 +58,21 @@ export function rmDir(dir, log) {
57
58
  }
58
59
 
59
60
  // This is basically so that we can dynamically import .js files by copying them to temp .mjs files, to avoid errors from node
60
- export async function copyFileToTempAndEnsureExtension({ filePath, tmpDir, ext }) {
61
+ export async function copyFileToTempAndEnsureExtension({ appLang, filePath, tmpDir, ext, log }) {
61
62
  try {
62
63
  const fileName = ensureExtension(path.basename(filePath), ext)
63
64
  const destPath = path.join(tmpDir, fileName)
64
65
  await fsp.copyFile(filePath, destPath)
65
66
  return destPath
66
67
  } catch (error) {
67
- log.E(`Error copying file to temp directory: ${error.message}`)
68
- throw error
68
+ throw new Error(
69
+ localizeFormatted({
70
+ token: 'error-copying-file-to-temp-dir',
71
+ data: { error: error.message },
72
+ lang: appLang,
73
+ log
74
+ })
75
+ )
69
76
  }
70
77
  }
71
78
 
@@ -24,6 +24,7 @@ export async function loadReferenceFile({ appLang, options: { referenceFile, ref
24
24
 
25
25
  // For .js, we need to copy to a temp location as an .mjs so we can dynamically import
26
26
  const tmpReferencePath = await copyFileToTempAndEnsureExtension({
27
+ appLang,
27
28
  filePath: referenceFile,
28
29
  tmpDir,
29
30
  ext: 'mjs',
package/src/main.mjs CHANGED
@@ -81,7 +81,7 @@ export async function run() {
81
81
  const command = this.name()
82
82
  const options = this.opts()
83
83
 
84
- if (options.logo) {
84
+ if (options.logo && process.env.ALT_TEST !== '1') {
85
85
  await printLogo({
86
86
  fontsSrcDir: path.resolve(__dirname, '../assets/figlet-fonts/'),
87
87
  tagline: p.description,
@@ -111,7 +111,7 @@ export async function run() {
111
111
  .option('-c, --config-file <path>', `Path to config file; defaults to <output dir>/${DEFAULT_CONFIG_FILENAME}`)
112
112
  .option('-rl, --reference-language <language>', `The reference file's language; overrides any 'referenceLanguage' config setting`)
113
113
  .option('-o, --output-dir <path>', 'Output directory for localized files')
114
- .option('-l, --target-languages <list>', `Comma-separated list of language codes; overrides any 'targetLanguages' config setting`, value => languageList(value, log))
114
+ .option('-tl, --target-languages <list>', `Comma-separated list of language codes; overrides any 'targetLanguages' config setting`, value => languageList(value, log))
115
115
  .option('-k, --keys <list>', 'Comma-separated list of keys to process', keyList)
116
116
  .option('-R, --reference-exported-var-name <var name>', `For .js or .mjs reference files, this will be the exported variable, e.g. for 'export default = {...}' you'd use 'default' here, or 'data' for 'export const data = { ... }'. For .json or .jsonc reference files, this value is ignored.`, 'default')
117
117
  .option('-m, --app-context-message <message>', `Description of your app to give context. Passed with each translation request; overrides any 'appContextMessage' config setting`)
package/test/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # ALT Testing Guide
2
+
3
+ This directory contains tests for the ALT localization tool, using Mocha, Chai, and Execa.
4
+
5
+ ## Test Structure
6
+
7
+ - `mock.test.js`: Simple tests that run without external dependencies
8
+ - `cli-translation.test.js`: Tests for the core translation CLI functionality
9
+ - `config.test.js`: Tests for configuration handling
10
+ - `list-models.test.js`: Tests for the list-models command
11
+ - `localization.test.js`: Tests for the localization system
12
+ - `main-cli.test.js`: Tests for the main CLI interface
13
+ - `translate-command.test.js`: Tests for the translate command
14
+
15
+ ## Setup
16
+ ANTHROPIC_API_KEY, GOOGLE_API_KEY, OPENAI_API_KEY must be set in order to run some tests.
17
+
18
+ ## Running Tests
19
+
20
+ Run all tests (requires API keys):
21
+ ```
22
+ npm test
23
+ ```
24
+
25
+ Run specific test:
26
+ ```
27
+ npx mocha --grep "test description"
28
+ ```
29
+
30
+ Run tests with coverage:
31
+ ```
32
+ npm run test:coverage
33
+ ```
34
+
35
+ ## Test Data
36
+
37
+ The `fixtures` directory contains test fixtures used by the tests.
@@ -0,0 +1,29 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { DEFAULT_CACHE_FILENAME } from '../src/lib/consts.js'
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = path.dirname(__filename)
8
+ export const SRC_DATA_DIR = path.join(__dirname, 'fixtures')
9
+
10
+ export function cleanupCacheFile(dir) {
11
+ cleanupFile(path.resolve(dir, DEFAULT_CACHE_FILENAME))
12
+ }
13
+
14
+ export function ensureDir(dir) {
15
+ if (fs.existsSync(dir)) return
16
+ fs.mkdirSync(dir, { recursive: true })
17
+ }
18
+
19
+ export function cleanupFile(file) {
20
+ if (!fs.existsSync(file)) return
21
+ fs.unlinkSync(file)
22
+ }
23
+
24
+ export function cleanupDir(dir) {
25
+ if (!fs.existsSync(dir)) return
26
+ fs.rmdirSync(dir, { recursive: true })
27
+ }
28
+
29
+
@@ -0,0 +1,78 @@
1
+ import { expect } from 'chai'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { dirname } from 'path'
6
+ import { execa } from 'execa'
7
+ import { cleanupCacheFile, cleanupDir, cleanupFile, ensureDir, SRC_DATA_DIR } from './common.mjs'
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = dirname(__filename)
11
+ const TEST_CONFIG_DIR = path.join(__dirname, 'test-config')
12
+
13
+ describe('config functionality', () => {
14
+ before(() => {
15
+ // Create test directory if it doesn't exist
16
+ ensureDir(TEST_CONFIG_DIR)
17
+ })
18
+
19
+ afterEach(() => {
20
+ // Clean up test config file after each test
21
+ const configPath = path.join(TEST_CONFIG_DIR, 'config.json')
22
+ cleanupFile(configPath)
23
+ })
24
+
25
+ after(() => {
26
+ cleanupDir(TEST_CONFIG_DIR)
27
+ cleanupCacheFile(SRC_DATA_DIR)
28
+ })
29
+
30
+ it('should use custom config file when specified', async function() {
31
+ this.timeout(5000)
32
+
33
+ // Create a custom config file
34
+ const configPath = path.join(TEST_CONFIG_DIR, 'config.json')
35
+ const configContent = {
36
+ provider: 'anthropic',
37
+ targetLanguages: [
38
+ 'fr-FR',
39
+ 'es-ES'
40
+ ],
41
+ lookForContextData: true,
42
+ contextPrefix: 'TEST_PREFIX',
43
+ contextSuffix: 'TEST_SUFFIX',
44
+ referenceLanguage: 'en',
45
+ normalizeOutputFilenames: true
46
+ }
47
+
48
+ fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2))
49
+
50
+ // Create a simple reference file for testing
51
+ const refPath = path.join(TEST_CONFIG_DIR, 'reference.js')
52
+ fs.writeFileSync(refPath, 'export default { "test-key": "This is a test" }')
53
+
54
+ try {
55
+ // Run the CLI with the custom config
56
+ const result = await execa('node', [
57
+ path.resolve(__dirname, '../alt.mjs'),
58
+ 'translate',
59
+ '-r',
60
+ refPath,
61
+ '--config-file',
62
+ configPath,
63
+ '--debug'
64
+ ])
65
+
66
+ // Command should succeed
67
+ expect(result.exitCode).to.equal(0)
68
+
69
+ // Output should include info from our config
70
+ expect(result.stdout).to.include('anthropic')
71
+ expect(result.stdout).to.include('fr-FR')
72
+ expect(result.stdout).to.include('es-ES')
73
+ } finally {
74
+ // Clean up reference file
75
+ cleanupFile(refPath)
76
+ }
77
+ })
78
+ })
@@ -0,0 +1,5 @@
1
+ export default {
2
+ 'msg-test': `Nothing to do`,
3
+ 'error-finished': `Finished with %%errorsEncountered%% error%%s%%`,
4
+ '_context:msg-finished': `This is a message displayed at the end of the app run`
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "msg-test": "Nothing to do",
3
+ "error-finished": "Finished with %%errorsEncountered%% error%%s%%",
4
+ "_context:msg-finished": "This is a message displayed at the end of the app run"
5
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ // Some great stuff here
3
+ "msg-test": "Nothing to do",
4
+ "error-finished": "Finished with %%errorsEncountered%% error%%s%%",
5
+
6
+ // Context keys
7
+ "_context:msg-finished": "This is a message displayed at the end of the app run"
8
+ }
@@ -0,0 +1,27 @@
1
+ import { execa } from 'execa'
2
+ import { expect } from 'chai'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import { fileURLToPath } from 'url'
6
+ import { dirname } from 'path'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = dirname(__filename)
10
+
11
+ describe('list-models command', () => {
12
+ it('should display models from a specific provider when specified', async () => {
13
+ // Run with specific provider (using anthropic as example)
14
+ const result = await execa('node', [
15
+ path.resolve(__dirname, '../alt.mjs'),
16
+ 'list-models',
17
+ '-p',
18
+ 'anthropic'
19
+ ])
20
+
21
+ // Check the command executed successfully
22
+ expect(result.exitCode).to.equal(0)
23
+
24
+ // Output should include provider-specific information
25
+ expect(result.stdout).to.include('Available models')
26
+ })
27
+ })
@@ -0,0 +1,124 @@
1
+ import { expect } from 'chai'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { dirname } from 'path'
6
+ import { isBcp47LanguageTagValid, isDefaultLanguage, initLocalizer } from '../src/localizer/localize.js'
7
+ import { LANGTAG_DEFAULT } from '../src/lib/consts.js'
8
+ import { ensureDir, cleanupDir } from './common.mjs'
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+ const TEST_LOCALIZATION_DIR = path.join(__dirname, 'test-localization')
13
+
14
+ describe('localization functionality', () => {
15
+ before(() => {
16
+ // Create test directory if it doesn't exist
17
+ ensureDir(TEST_LOCALIZATION_DIR)
18
+
19
+ // Create a simple English localization file
20
+ const enContent = {
21
+ "test_key": "This is a test",
22
+ "hello_world": "Hello, World!",
23
+ "formatted_string": "Hello, %%name%%!"
24
+ }
25
+ fs.writeFileSync(path.join(TEST_LOCALIZATION_DIR, 'en.json'), JSON.stringify(enContent, null, 2))
26
+
27
+ // Create a simple French localization file
28
+ const frContent = {
29
+ "test_key": "C'est un test",
30
+ "hello_world": "Bonjour, Monde!",
31
+ "formatted_string": "Bonjour, %%name%%!"
32
+ }
33
+ fs.writeFileSync(path.join(TEST_LOCALIZATION_DIR, 'fr-FR.json'), JSON.stringify(frContent, null, 2))
34
+ })
35
+
36
+ after(() => {
37
+ cleanupDir(TEST_LOCALIZATION_DIR)
38
+ })
39
+
40
+ describe('isBcp47LanguageTagValid', () => {
41
+ it('should validate valid BCP47 language tags', () => {
42
+ expect(isBcp47LanguageTagValid('en')).to.be.an('object')
43
+ expect(isBcp47LanguageTagValid('fr-FR')).to.be.an('object')
44
+ expect(isBcp47LanguageTagValid('de-DE')).to.be.an('object')
45
+ expect(isBcp47LanguageTagValid('zh-Hans')).to.be.an('object')
46
+ })
47
+
48
+ it('should reject invalid BCP47 language tags', () => {
49
+ expect(isBcp47LanguageTagValid('not-a-language')).to.be.undefined
50
+ expect(isBcp47LanguageTagValid('xx-XX')).to.be.undefined
51
+ expect(isBcp47LanguageTagValid('')).to.be.undefined
52
+ })
53
+ })
54
+
55
+ describe('isDefaultLanguage', () => {
56
+ it('should identify the default language', () => {
57
+ expect(isDefaultLanguage(LANGTAG_DEFAULT)).to.be.true
58
+ })
59
+
60
+ it('should reject non-default languages', () => {
61
+ expect(isDefaultLanguage('fr-FR')).to.be.false
62
+ expect(isDefaultLanguage('es-ES')).to.be.false
63
+ })
64
+ })
65
+
66
+ describe('initLocalizer', () => {
67
+ it('should initialize with the default language', async () => {
68
+ const mockLog = {
69
+ D: () => {},
70
+ W: () => {},
71
+ E: () => {},
72
+ I: () => {},
73
+ V: () => {}
74
+ }
75
+
76
+ const lang = await initLocalizer({
77
+ defaultAppLanguage: 'en',
78
+ appLanguage: null,
79
+ srcDir: TEST_LOCALIZATION_DIR,
80
+ log: mockLog
81
+ })
82
+
83
+ expect(lang).to.equal('en')
84
+ })
85
+
86
+ it('should initialize with a specified language', async () => {
87
+ const mockLog = {
88
+ D: () => {},
89
+ W: () => {},
90
+ E: () => {},
91
+ I: () => {},
92
+ V: () => {}
93
+ }
94
+
95
+ const lang = await initLocalizer({
96
+ defaultAppLanguage: 'en',
97
+ appLanguage: 'fr-FR',
98
+ srcDir: TEST_LOCALIZATION_DIR,
99
+ log: mockLog
100
+ })
101
+
102
+ expect(lang).to.equal('fr-FR')
103
+ })
104
+
105
+ it('should fall back to default language when invalid language specified', async () => {
106
+ const mockLog = {
107
+ D: () => {},
108
+ W: () => {},
109
+ E: () => {},
110
+ I: () => {},
111
+ V: () => {}
112
+ }
113
+
114
+ const lang = await initLocalizer({
115
+ defaultAppLanguage: 'en',
116
+ appLanguage: 'invalid-language',
117
+ srcDir: TEST_LOCALIZATION_DIR,
118
+ log: mockLog
119
+ })
120
+
121
+ expect(lang).to.equal('en')
122
+ })
123
+ })
124
+ })
@@ -0,0 +1,79 @@
1
+ import { execa } from 'execa'
2
+ import { expect } from 'chai'
3
+ import { fileURLToPath } from 'url'
4
+ import { dirname } from 'path'
5
+ import { cleanupCacheFile, SRC_DATA_DIR } from './common.mjs'
6
+ import path from 'path'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = dirname(__filename)
10
+
11
+ describe('main CLI functionality', () => {
12
+ after(() => cleanupCacheFile(SRC_DATA_DIR))
13
+
14
+ it('should display help information', async () => {
15
+ // Run the help command
16
+ const result = await execa('node', [
17
+ path.resolve(__dirname, '../alt.mjs'),
18
+ '--help'
19
+ ])
20
+
21
+ // Check the command executed successfully
22
+ expect(result.exitCode).to.equal(0)
23
+
24
+ // Output should contain expected help information
25
+ expect(result.stdout).to.include('Usage:')
26
+ expect(result.stdout).to.include('Options:')
27
+ expect(result.stdout).to.include('Commands:')
28
+ expect(result.stdout).to.include('Environment variables:')
29
+ })
30
+
31
+ it('should display version information', async () => {
32
+ // Run the version command
33
+ const result = await execa('node', [
34
+ path.resolve(__dirname, '../alt.mjs'),
35
+ '--version'
36
+ ])
37
+
38
+ // Check the command executed successfully
39
+ expect(result.exitCode).to.equal(0)
40
+
41
+ // Output should contain version number
42
+ expect(result.stdout).to.match(/\d+\.\d+\.\d+/)
43
+ })
44
+
45
+ it('should display command-specific help', async () => {
46
+ // Run the help command for translate
47
+ const result = await execa('node', [
48
+ path.resolve(__dirname, '../alt.mjs'),
49
+ 'help',
50
+ 'translate'
51
+ ])
52
+
53
+ // Check the command executed successfully
54
+ expect(result.exitCode).to.equal(0)
55
+
56
+ // Output should contain expected help information for translate command
57
+ expect(result.stdout).to.include('Usage: alt translate')
58
+ expect(result.stdout).to.include('-r, --reference-file')
59
+ expect(result.stdout).to.include('-tl, --target-languages')
60
+ })
61
+
62
+ it('should return error for missing required options', async () => {
63
+ try {
64
+ // Run translate command without required options
65
+ await execa('node', [
66
+ path.resolve(__dirname, '../alt.mjs'),
67
+ 'translate'
68
+ ])
69
+ // Should not reach here as the command should fail
70
+ expect.fail('Command should have failed with missing required options')
71
+ } catch (error) {
72
+ // Check that the command failed
73
+ expect(error.exitCode).to.not.equal(0)
74
+
75
+ // Error should mention missing required option
76
+ expect(error.stderr).to.include('required option')
77
+ }
78
+ })
79
+ })
@@ -0,0 +1,10 @@
1
+ // Mocha setup file
2
+ // This file sets up the environment for testing
3
+
4
+ // Set timeout for all tests to 10 seconds by default
5
+ export const mochaHooks = {
6
+ beforeAll() {
7
+ // Default timeout for tests (can be overridden in individual tests)
8
+ this.timeout(10000);
9
+ }
10
+ };