npm package: @locamorph/translation-processor
Description: Parse and export translation files in multiple formats
Latest version: 1.0.0

# @locamorph/translation-processor

Parse and export translation files in multiple formats. Works in both browser and Node.js.

## Installation

```bash
npm install @locamorph/translation-processor
```

## Supported Formats

| Format | Parser | Exporter |
|--------|--------|----------|
| JSON | ✅ | ✅ |
| ARB | ✅ | - |
| YAML | ✅ | - |
| Android XML | ✅ | - |
| iOS Strings | ✅ | - |
| Properties | ✅ | - |
| XLIFF | ✅ | - |

## Usage

### Parsing Content

```typescript
import { parseContent, detectFileFormat } from '@locamorph/translation-processor';

// Auto-detect format and parse
const format = detectFileFormat(content);
const keys = parseContent(content, format);
// => [{ key: 'greeting', value: 'Hello' }, ...]
```

### Exporting Translations

```typescript
import { exportContent } from '@locamorph/translation-processor';

const translations = {
  greeting: {
    file: 'default',
    key: 'greeting',
    val: 'Hello',
    opts: [1, 0, null],
    meta: ['2024-01-01', '2024-01-01'],
  },
};

const baseTranslations = translations; // For base language, same as translations

const content = exportContent(translations, {
  format: 'json',
  options: {
    pluralFormat: 'icu',
    indentation: '2',
    nestedKeysEnabled: true,
    nestedKeysSeparator: '.',
    sortKeysBy: 'key-name-az',
    emptyTranslations: 'dont-export',
    convertLineBreaks: true,
    addNewlineAtEof: true,
  },
  baseTranslations,
});
```

### Generating Multiple Files

```typescript
import { generateExportFiles, LanguageExportData } from '@locamorph/translation-processor';

const baseTranslations = {
  common: {
    greeting: { file: 'common', key: 'greeting', val: 'Hello', opts: [1, 0, null], meta: ['2024-01-01', '2024-01-01'] },
  },
};

const languagesData: LanguageExportData[] = [
  {
    languageCode: 'en',
    translations: baseTranslations,
    baseTranslations: baseTranslations,
  },
  {
    languageCode: 'es',
    translations: {
      common: {
        greeting: { file: 'common', key: 'greeting', val: 'Hola', opts: [1, 0, null], meta: ['2024-01-02', '2024-01-02'] },
      },
    },
    baseTranslations: baseTranslations,
  },
];

// Single file per language (no {FILE} in pattern)
const files = generateExportFiles(languagesData, {
  format: 'json',
  options: exportOptions,
  fileStructure: 'locale/{LANG_ISO}.{FORMAT}',
});
// => [{ path: 'locale/en.json', content: '{"greeting":"Hello"}' }, ...]

// Multiple files per language (pattern contains {FILE})
const multiFiles = generateExportFiles(languagesData, {
  format: 'json',
  options: exportOptions,
  fileStructure: '{LANG_ISO}/{FILE}',
});
// => [{ path: 'en/common.json', content: '...' }, ...]
```

### Two-Step Export (process then serialize)

```typescript
import { generateExportData, serializeTranslations } from '@locamorph/translation-processor';

// Step 1: Get processed data objects
const files = generateExportData(languagesData, config);
// => [{ path: 'locale/en.json', data: { greeting: 'Hello' } }, ...]

// Step 2: Serialize to target format
files.forEach(file => {
  const content = serializeTranslations(file.data, {
    format: 'json',
    options: { indentation: '2', addNewlineAtEof: true }
  });
  // write content to file.path...
});
```

## API Reference

### Detection Functions

- `detectFileFormat(content)` - Detect format from content
- `detectFileFormatWithConfidence(content)` - Detect with confidence score
- `detectLanguageFromContent(content, format)` - Extract language code
- `detectFormatFromExtension(extension)` - Get format from file extension
- `detectLanguageFromFilename(filename)` - Extract language from filename

### Parsing Functions

- `parseContent(content, format)` - Parse content to translation keys

### Import Functions

- `parseFileToTranslations(content, filePath, fileName?)` - Parse file content and extract translations as flat key-value pairs

### Export Functions

- `generateExportFiles(languagesData, config)` - Generate serialized files for multiple languages
- `generateExportData(languagesData, config)` - Generate processed data objects (not serialized)
- `serializeTranslations(data, config)` - Serialize processed data to target format (JSON, etc.)
- `exportContent(translations, config)` - Export single file to specified format
- `exportToJson(translations, options, baseTranslations)` - Direct JSON export
- `getDefaultFilePattern(single)` - Get default file path pattern (`true` = single file, `false` = multiple files)
- `validateFilePattern(pattern)` - Validate a file path pattern
- `validateSourceFilePattern(pattern)` - Validate a source file glob pattern
- `resolveFilePath(pattern, languageCode, format, fileName?)` - Resolve placeholders in a pattern
- `parseFilePath(filePath, basePath, fileStructure)` - Extract values from a file path (reverse of resolveFilePath)
- `createNamespace(input, basePath?)` - Create a namespace from a file path for use in translation file paths

### File Path Placeholders

Use `getDefaultFilePattern(single)` to get the default pattern:

```typescript
import { getDefaultFilePattern } from '@locamorph/translation-processor';

getDefaultFilePattern(true)   // => '{LANG_ISO}.{FORMAT}'
getDefaultFilePattern(false)  // => '{LANG_ISO}/{FILE}'
```

When configuring `fileStructure` in `ExportFileConfig`, use these placeholders:

| Placeholder | Description | Example |
|-------------|-------------|---------|
| `{LANG_ISO}` | Language code | `en`, `es`, `fr` |
| `{FORMAT}` | File format extension | `json`, `yaml` |
| `{FILE}` | File name with format extension | `common.json` |

The mode is automatically determined:
- **Single file mode**: Pattern does NOT contain `{FILE}` → all translations merged into one file per language
- **Multiple file mode**: Pattern contains `{FILE}` → one file per source file per language

**Examples:**

```typescript
// Single file per language: locale/en.json, locale/es.json
fileStructure: 'locale/{LANG_ISO}.{FORMAT}'

// Multiple files per language: en/common.json, en/errors.json
fileStructure: '{LANG_ISO}/{FILE}'

// Custom structures
fileStructure: 'i18n/{LANG_ISO}/messages.{FORMAT}'  // Single: i18n/en/messages.json
fileStructure: 'locales/{LANG_ISO}/{FILE}'          // Multiple: locales/en/common.json
```

**Validation & Parsing:**

```typescript
import { validateFilePattern, resolveFilePath, parseFilePath } from '@locamorph/translation-processor';

// Validate patterns before use
const result = validateFilePattern('{LANG_ISO}/{FILE}');
if (!result.valid) {
  console.error(result.error);
}

// Resolve placeholders (pattern → path)
resolveFilePath('{LANG_ISO}.{FORMAT}', 'en', 'json')           // => 'en.json'
resolveFilePath('{LANG_ISO}/{FILE}', 'en', 'json', 'common')   // => 'en/common.json'

// Parse file paths (path → values) - reverse of resolveFilePath
parseFilePath('/project/locales/de/default.json', '/project/locales', '{LANG_ISO}/{FILE}')
// => { langIso: 'de', file: 'default', format: 'json' }

parseFilePath('/project/locales/de/nested/path/file.json', '/project/locales', '{LANG_ISO}/{FILE}')
// => { langIso: 'de', file: 'nested/path/file', format: 'json' }

parseFilePath('/project/i18n/en.json', '/project/i18n', '{LANG_ISO}.{FORMAT}')
// => { langIso: 'en', format: 'json' }
```

Validation rules:
- No leading or trailing `/` or `.` characters
- Must contain `{LANG_ISO}` or `{FILE}` placeholder
- Only valid placeholders: `{LANG_ISO}`, `{FORMAT}`, `{FILE}`
- Only alphanumeric, hyphen, underscore, dot, slash characters allowed

**Namespace Creation:**

Use `createNamespace(input, basePath?)` to create a namespace from a file path:

```typescript
import { createNamespace } from '@locamorph/translation-processor';

createNamespace('nested/path/file')       // => 'nested/path/file'
createNamespace('nested/path/file.json')  // => 'nested/path/file'
createNamespace('/nested/path/')          // => 'nested/path'
createNamespace('test.json')              // => 'test'
createNamespace('.hidden')                // => 'hidden'
createNamespace('file name')              // => 'file-name'
createNamespace('path//double//slash')    // => 'path/double/slash'

// With basePath (strips prefix before normalizing):
createNamespace('nested/path/file', 'nested')        // => 'path/file'
createNamespace('nested/path/file.json', '/nested')  // => 'path/file'
createNamespace('nested/path/file.json', './nested') // => 'path/file'
createNamespace('nested/path/file.json', 'nested/')  // => 'path/file'
createNamespace('/nested/path/', '/nested')          // => 'path'
createNamespace('nested/test.json', '/nested')       // => 'test'
```

Normalization rules (applied in order):
1. Strip basePath prefix if provided
2. Trim whitespace
3. Replace spaces with hyphens
4. Remove invalid characters (keep only alphanumeric, hyphen, underscore, forward slash)
5. Remove file extension if present (`.json`, `.yaml`, `.xml`, etc.)
6. Remove leading/trailing `/` and `.` characters
7. Collapse multiple consecutive slashes

**Importing Translations:**

Use `parseFileToTranslations(content, filePath, fileName?)` to parse file content and extract translations:

```typescript
import { parseFileToTranslations } from '@locamorph/translation-processor';

const content = '{ "hello": "world", "greeting": "Hello!" }';
const translations = parseFileToTranslations(content, '/locales/en/common.json');
// Returns:
// [
//   { key: 'hello', value: 'world', file: 'common' },
//   { key: 'greeting', value: 'Hello!', file: 'common' }
// ]

// Override the file namespace:
const translations2 = parseFileToTranslations(content, '/locales/en/common.json', 'messages');
// Returns:
// [
//   { key: 'hello', value: 'world', file: 'messages' },
//   { key: 'greeting', value: 'Hello!', file: 'messages' }
// ]
```

- Format is auto-detected from file extension
- Returns empty array if format is unsupported or parsing fails
- The `file` property is extracted from the filename (without extension) unless overridden

**Source File Pattern Validation:**

Use `validateSourceFilePattern(pattern)` to validate glob patterns for source file matching:

```typescript
import { validateSourceFilePattern } from '@locamorph/translation-processor';

// Valid patterns
validateSourceFilePattern('translations/**/*.json')     // => { valid: true }
validateSourceFilePattern('**/*.{json,yaml}')           // => { valid: true }
validateSourceFilePattern('src/locales/**/*.yaml')      // => { valid: true }
validateSourceFilePattern('!node_modules/**/*.json')    // => { valid: true }

// Invalid patterns
validateSourceFilePattern('*.*')          // => { valid: false, error: '... too broad ...' }
validateSourceFilePattern('src/*')        // => { valid: false, error: '... must specify extension ...' }
validateSourceFilePattern('src/')         // => { valid: false, error: '... cannot end with "/" ...' }
validateSourceFilePattern('$HOME/*.json') // => { valid: false, error: '... invalid characters ...' }
validateSourceFilePattern('**/*.txt')     // => { valid: false, error: '... Unsupported file extension ...' }
```

Validation rules:
- Cannot be empty
- No trailing `/` character
- Cannot use overly broad patterns (`*.*`, `**/*.*`, `*`, `**`)
- Must specify a file extension (e.g., `*.json` not just `*`)
- Extension must be a supported format (json, yaml, yml, xml, properties, strings, arb, xliff, xlf)
- Only safe characters allowed: alphanumeric, `-`, `_`, `.`, `/`, and glob chars (`*`, `?`, `[]`, `{}`, `!`, `,`)

### Format Configuration

- `FILE_FORMATS` - All supported format configs
- `getSupportedFormats()` - Formats with parsers
- `getExportableFormats()` - Formats with exporters
- `getFormatById(id)` - Get format config by ID

## License

MIT