@wads.dev/i18n-ts 0.0.1-alpha.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.
@@ -0,0 +1,52 @@
1
+ # i18n project architecture for AI agents
2
+
3
+ Read this document before changing a consuming project's translations. The host project's own instructions still take precedence.
4
+
5
+ ## Discover the project first
6
+
7
+ 1. Read the repository instructions and its root `i18n.config.json` when present.
8
+ 2. Locate the root translation type, language catalog and locale implementations. Do not assume fixed filenames.
9
+ 3. Inspect the existing runtime integration before changing component usage. It may be React, Swift, Kotlin or another platform.
10
+ 4. List the configured languages and grouping levels before editing files.
11
+
12
+ `i18n.config.json` describes how translation paths map to the repository:
13
+
14
+ - `levelCount` defines how many leading key segments are structural groups, excluding root.
15
+ - `levelNames` gives those groups human-readable names.
16
+ - `levelImports[0]` describes the root translation location without consuming a key segment.
17
+ - `levelImports[1...levelCount]` map group values to directories and may define `valueReplacer` and `fullReplacer` rules.
18
+ - `importAliases` resolves project-root prefixes; unaliased level paths are relative to their parent.
19
+ - `translationsDirectory` is the locale directory inside the resolved group.
20
+ - `languageFileTemplate` maps a language identifier to its file.
21
+ - `languageReplacer` maps a catalog language key to a different filename when required.
22
+
23
+ Replacers accept dotted full paths or nested objects. `valueReplacer` changes the value inserted in `{value}`. A string in `fullReplacer` redirects a subtree, while `null` keeps the subtree in the parent level's translation files.
24
+
25
+ Use defaults from `@wads.dev/i18n-ts/config` only when the project does not provide a value.
26
+
27
+ ## Translation contracts
28
+
29
+ - A root TypeScript contract describes the complete translation tree.
30
+ - Every locale must implement the same structure.
31
+ - Nested records represent namespaces; strings represent static text.
32
+ - Parameterized text uses typed functions with identical signatures in every locale.
33
+ - Renderable or tokenized values use the host runtime's typed render mechanism.
34
+ - Keep the catalog metadata (`name`, `short`, `locale` and loader) synchronized with the locale files.
35
+
36
+ ## Editing rules
37
+
38
+ - Never leave user-visible text hardcoded when the host project requires i18n.
39
+ - Search for an existing semantically equivalent key before creating another.
40
+ - Put a key in the closest configured group that owns its meaning.
41
+ - Update the type contract first, then every locale implementation.
42
+ - Preserve function parameters, token names, nesting and value kinds across languages.
43
+ - Translate intent rather than words literally; preserve the product's terminology and tone.
44
+ - Do not silently replace copy in languages you cannot translate reliably. Report the limitation.
45
+ - Validate with the host project's typecheck, tests and lint commands.
46
+
47
+ ## Package boundaries
48
+
49
+ - `@wads.dev/i18n-ts` provides framework-independent runtime types and language loading.
50
+ - `@wads.dev/i18n-ts/bundle` provides the portable bundle contract.
51
+ - `@wads.dev/i18n-ts/config` provides the portable project-configuration contract.
52
+ - Framework adapters and editor operations are separate packages. Discover their APIs in the consuming project rather than assuming one platform.
package/AI/DEV_USE.md ADDED
@@ -0,0 +1,17 @@
1
+ # Using the AI guides
2
+
3
+ Always begin with `ARCHITECTURE.md`, then select the task guide:
4
+
5
+ - `PROMPT_TRANSLATE_FILE.md` — move user-visible literals from source code into translations.
6
+ - `PROMPT_ADD_LANG_TO_PROJECT.md` — add a language to the complete catalog.
7
+ - `PROMPT_CREATE_TRANSLATION_KEY.md` — create one or more typed keys in every locale.
8
+ - `PROMPT_UPDATE_TRANSLATION.md` — change existing copy consistently.
9
+
10
+ Example:
11
+
12
+ ```text
13
+ Read node_modules/@wads.dev/i18n-ts/AI/ARCHITECTURE.md and then follow
14
+ node_modules/@wads.dev/i18n-ts/AI/PROMPT_TRANSLATE_FILE.md for <source-file>.
15
+ ```
16
+
17
+ These documents describe portable behavior. Repository instructions, `i18n.config.json` and existing code determine the actual paths and platform integration.
@@ -0,0 +1,27 @@
1
+ # Add a language to a project
2
+
3
+ Read `ARCHITECTURE.md` first.
4
+
5
+ ## Objective
6
+
7
+ Add one locale while preserving the complete typed structure and the conventions of the consuming project.
8
+
9
+ ## Procedure
10
+
11
+ 1. Read `i18n.config.json`, the language catalog, root contract and every existing locale.
12
+ 2. Confirm the language key, display name, short identifier and standards-based locale identifier.
13
+ 3. Use the most complete existing locale as the structural reference.
14
+ 4. Create the new locale file in every configured translation group that has locale implementations.
15
+ 5. Preserve nesting, function signatures, tokens and renderable value shapes exactly.
16
+ 6. Translate all values with terminology consistent with the product. Do not leave source-language text disguised as a completed translation.
17
+ 7. Add the locale metadata and loader to the catalog using the existing pattern.
18
+ 8. Search for fixed language lists or locale-specific branches and update only those that should support the new locale.
19
+ 9. Run typecheck, tests and lint.
20
+
21
+ ## Completion checklist
22
+
23
+ - [ ] Every configured translation group has the new locale where applicable.
24
+ - [ ] The new locale satisfies the root and nested TypeScript contracts.
25
+ - [ ] Catalog metadata and loader are complete.
26
+ - [ ] Functions and tokens match every other locale.
27
+ - [ ] No language whitelist unintentionally excludes the new locale.
@@ -0,0 +1,26 @@
1
+ # Create translation keys
2
+
3
+ Read `ARCHITECTURE.md` first.
4
+
5
+ ## Objective
6
+
7
+ Add the requested translation paths to the correct typed contract and every supported locale.
8
+
9
+ ## Procedure
10
+
11
+ 1. Read `i18n.config.json` and map each requested path to its configured structural group.
12
+ 2. Search for an existing key with the same meaning before creating a duplicate.
13
+ 3. Inspect neighboring keys to preserve naming, ordering, terminology and value type.
14
+ 4. Add the key to the closest owning TypeScript contract.
15
+ 5. Implement the identical shape in every locale file belonging to that group.
16
+ 6. Use typed functions for interpolation and the host integration's typed render mechanism for rich content.
17
+ 7. Keep function parameters, token names and nesting identical across locales.
18
+ 8. Update consuming code if the requested change includes usage.
19
+ 9. Run typecheck, tests and lint.
20
+
21
+ ## Completion checklist
22
+
23
+ - [ ] Each key belongs to the correct configured group.
24
+ - [ ] The type contract and every locale expose the same shape.
25
+ - [ ] Copy is meaningful in every language.
26
+ - [ ] No duplicate semantic key or temporary placeholder remains.
@@ -0,0 +1,25 @@
1
+ # Move a source file's text to i18n
2
+
3
+ Read `ARCHITECTURE.md` first.
4
+
5
+ ## Objective
6
+
7
+ Replace human-facing literals in the requested source file with typed translations that follow the consuming project's existing runtime integration.
8
+
9
+ ## Procedure
10
+
11
+ 1. Read `i18n.config.json`, the target file and the nearest project architecture instructions.
12
+ 2. Determine the owning translation group from the configured levels and the file's product responsibility.
13
+ 3. Find all user-visible literals, including labels, validation messages, accessibility text and user-facing errors. Do not move protocol values, identifiers, test fixtures or debug-only logs.
14
+ 4. Search for reusable existing keys before creating new ones.
15
+ 5. Add missing keys to the owning contract and every locale, following `PROMPT_CREATE_TRANSLATION_KEY.md`.
16
+ 6. Replace literals using the runtime pattern already present in the project. Do not invent a new hook, provider or accessor.
17
+ 7. Model interpolation with typed functions and rich content with the existing typed render mechanism.
18
+ 8. Remove obsolete imports and run typecheck, tests and lint.
19
+
20
+ ## Completion checklist
21
+
22
+ - [ ] No applicable user-facing literal remains in the target file.
23
+ - [ ] Every new key exists with the same shape in every locale.
24
+ - [ ] Existing runtime conventions were preserved.
25
+ - [ ] Dynamic values remain typed and safe.
@@ -0,0 +1,22 @@
1
+ # Update translation values
2
+
3
+ Read `ARCHITECTURE.md` first.
4
+
5
+ ## Objective
6
+
7
+ Change existing copy in every locale while preserving its typed structure and semantic intent.
8
+
9
+ ## Procedure
10
+
11
+ 1. Resolve each requested key through `i18n.config.json` and confirm its current contract.
12
+ 2. Update every locale that implements the key.
13
+ 3. Preserve the value kind: strings remain strings; functions keep the same parameters; rich values keep the same token names and render contract.
14
+ 4. Translate the new intent consistently with each locale's terminology, punctuation and tone.
15
+ 5. Search for comments, tests or related copy that intentionally mirrors the old wording and update them when appropriate.
16
+ 6. Run typecheck, tests and lint.
17
+
18
+ ## Completion checklist
19
+
20
+ - [ ] Every locale reflects the requested semantic change.
21
+ - [ ] Function signatures, parameter order and token names are unchanged.
22
+ - [ ] No applicable stale wording remains.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wads.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @wads.dev/i18n-ts
2
+
3
+ Framework-independent TypeScript foundations for strongly typed internationalization.
4
+
5
+ `i18n-ts` gives a project one translation contract shared by every language, lazy language loading, a portable JSON bundle format and a machine-readable project configuration. It has no runtime dependencies and no React, DOM or application-specific coupling.
6
+
7
+ > Status: `0.0.1-alpha.0`. Public APIs and JSON formats may change before `1.0.0`.
8
+
9
+ ## Why it exists
10
+
11
+ Translation files are application code: their structure changes, keys move, languages grow and mistakes should fail during development rather than reach users. This package keeps that foundation small and explicit:
12
+
13
+ - TypeScript verifies that locales implement compatible structures.
14
+ - Dynamic translations keep the same function signature in every language.
15
+ - Languages can be loaded lazily and their resulting trees are immutable.
16
+ - A JSON bundle lets editors and tools work without importing the application runtime.
17
+ - `i18n.config.json` explains project structure to CLIs, editors and AI agents.
18
+
19
+ Platform integrations are separate. React support belongs to `@wads.dev/i18n-react`; translation editing belongs to `@wads.dev/i18n-editor`. The same contracts can support future Swift and Kotlin runtimes.
20
+
21
+ ## Installation
22
+
23
+ Install the current alpha explicitly:
24
+
25
+ ```sh
26
+ npm install @wads.dev/i18n-ts@alpha
27
+ ```
28
+
29
+ After a stable release:
30
+
31
+ ```sh
32
+ npm install @wads.dev/i18n-ts
33
+ ```
34
+
35
+ ## Define a typed catalog
36
+
37
+ Create the contract shared by all locales:
38
+
39
+ ```ts
40
+ import type { Translation } from '@wads.dev/i18n-ts'
41
+
42
+ export interface AppTranslation extends Translation {
43
+ commons: {
44
+ continue: string
45
+ }
46
+ cart: {
47
+ itemCount: (count: number) => string
48
+ }
49
+ }
50
+ ```
51
+
52
+ Implement it once per language:
53
+
54
+ ```ts
55
+ import type { AppTranslation } from '../base.js'
56
+
57
+ const en: AppTranslation = {
58
+ commons: { continue: 'Continue' },
59
+ cart: { itemCount: (count) => `${count} items` },
60
+ }
61
+
62
+ export default en
63
+ ```
64
+
65
+ ```ts
66
+ import type { AppTranslation } from '../base.js'
67
+
68
+ const pt: AppTranslation = {
69
+ commons: { continue: 'Continuar' },
70
+ cart: { itemCount: (count) => `${count} itens` },
71
+ }
72
+
73
+ export default pt
74
+ ```
75
+
76
+ The compiler now catches missing keys, incompatible nested structures and different function parameters.
77
+
78
+ ## Register and load languages
79
+
80
+ ```ts
81
+ import { loadLanguage, type AvailableLangs } from '@wads.dev/i18n-ts'
82
+ import type { AppTranslation } from './base.js'
83
+
84
+ const languages = {
85
+ en: {
86
+ name: 'English',
87
+ short: 'EN',
88
+ locale: 'en-US',
89
+ lang: () => import('./translations/en.js'),
90
+ },
91
+ pt: {
92
+ name: 'Português',
93
+ short: 'PT',
94
+ locale: 'pt-BR',
95
+ lang: () => import('./translations/pt.js'),
96
+ },
97
+ } satisfies AvailableLangs<'en' | 'pt', AppTranslation>
98
+
99
+ const selected = await loadLanguage(languages, 'en', 'pt-BR')
100
+ selected.lang.commons.continue
101
+ ```
102
+
103
+ `loadLanguage` selects an available language, loads it when necessary and deeply freezes the translation tree. Selection accepts an explicit language and otherwise falls back through the environment locale to the configured default.
104
+
105
+ ## Configure the project structure
106
+
107
+ Add `i18n.config.json` to the repository root. The application runtime does not need this file: it exists primarily to describe the project to AI agents and to `@wads.dev/i18n-editor`.
108
+
109
+ The Editor uses it to understand which key segments represent modules or features, how object names map to real folders, where root translations belong and which files should be regenerated after editing. This keeps structural knowledge outside application code.
110
+
111
+ See [Project configuration](./docs/CONFIGURATION.md) for the complete schema, replacers, aliases and path-resolution examples.
112
+
113
+ ## Export a portable bundle
114
+
115
+ The bundle is a portable, JSON-safe snapshot of the complete translation catalog, including language metadata, values and `updatedAt`. It separates translation review from the source-code layout.
116
+
117
+ ```sh
118
+ npx i18n-bundle \
119
+ --input src/i18n/index.ts \
120
+ --config i18n.config.json \
121
+ --output i18n.bundle.json
122
+ ```
123
+
124
+ The bundle can be opened by `@wads.dev/i18n-editor` or shared with translators without giving them the repository or asking them to navigate many TypeScript files. After review, the edited bundle can return to the development team and be redistributed into the configured project structure. The resulting source changes remain ordinary files, so the Git diff becomes the final safety and review layer before merge.
125
+
126
+ The alpha currently supports bundle generation, visualization and in-memory editing. Regenerating the complete TypeScript file tree from a reviewed bundle is the next Editor/CLI integration step; the configuration and export plan already describe those destinations.
127
+
128
+ See [Bundle and translation-review workflow](./docs/BUNDLE_WORKFLOW.md) for responsibilities, handoff stages and validation guidance.
129
+
130
+ The input module must export `Langs`. The command requires TypeScript in the consuming project when the catalog is written in TypeScript. When `--config` is supplied, a normalized `i18n.config.json` is copied beside the generated bundle so a separately served editor can load it automatically.
131
+
132
+ Bundle APIs are available separately:
133
+
134
+ ```ts
135
+ import {
136
+ assertBundle,
137
+ parseBundle,
138
+ type I18nBundle,
139
+ } from '@wads.dev/i18n-ts/bundle'
140
+ ```
141
+
142
+ Functions inside translation trees are serialized as descriptors so editors can inspect them without executing application code.
143
+
144
+ ## AI agent guides
145
+
146
+ The published package includes portable instructions under `AI/`. Projects can direct agents to these files instead of copying i18n instructions into every repository:
147
+
148
+ ```text
149
+ node_modules/@wads.dev/i18n-ts/AI/ARCHITECTURE.md
150
+ node_modules/@wads.dev/i18n-ts/AI/PROMPT_TRANSLATE_FILE.md
151
+ node_modules/@wads.dev/i18n-ts/AI/PROMPT_CREATE_TRANSLATION_KEY.md
152
+ node_modules/@wads.dev/i18n-ts/AI/PROMPT_UPDATE_TRANSLATION.md
153
+ node_modules/@wads.dev/i18n-ts/AI/PROMPT_ADD_LANG_TO_PROJECT.md
154
+ ```
155
+
156
+ The guides begin by reading the host repository instructions and `i18n.config.json`; they do not assume React or a fixed project layout.
157
+
158
+ ## Public entry points
159
+
160
+ | Import | Responsibility |
161
+ | --- | --- |
162
+ | `@wads.dev/i18n-ts` | Runtime types and `loadLanguage`. |
163
+ | `@wads.dev/i18n-ts/bundle` | Portable bundle types, validation and serialization. |
164
+ | `@wads.dev/i18n-ts/config` | Project configuration types, defaults and normalization. |
165
+
166
+ The root import deliberately excludes editor and CLI concerns.
167
+
168
+ ## Development
169
+
170
+ ```sh
171
+ npm install
172
+ npm run check
173
+ npm run build
174
+ npm pack --dry-run
175
+ ```
176
+
177
+ The source is entirely TypeScript. The build produces ESM JavaScript and declaration files in `dist/`.
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,27 @@
1
+ export declare const BUNDLE_FORMAT: "wads-i18n-bundle";
2
+ export declare const BUNDLE_VERSION: 2;
3
+ export type BundleFunction = {
4
+ $type: 'function';
5
+ source: string;
6
+ };
7
+ export type BundleValue = string | number | boolean | null | BundleFunction | BundleValue[] | BundleRecord;
8
+ export type BundleRecord = {
9
+ [key: string]: BundleValue;
10
+ };
11
+ export type BundleLanguage = {
12
+ name: string;
13
+ short: string;
14
+ locale: string;
15
+ translations: BundleRecord;
16
+ };
17
+ export type I18nBundle = {
18
+ format: typeof BUNDLE_FORMAT;
19
+ version: typeof BUNDLE_VERSION;
20
+ updatedAt: number;
21
+ languages: Record<string, BundleLanguage>;
22
+ };
23
+ export declare function createBundle(languages?: I18nBundle['languages'], updatedAt?: number): I18nBundle;
24
+ export declare function assertBundle(value: unknown): I18nBundle;
25
+ export declare function withUpdatedAt(bundle: I18nBundle, updatedAt?: number): I18nBundle;
26
+ export declare function parseBundle(text: string): I18nBundle;
27
+ export declare function serializeBundleValue(value: unknown): BundleValue;
@@ -0,0 +1,62 @@
1
+ export const BUNDLE_FORMAT = 'wads-i18n-bundle';
2
+ export const BUNDLE_VERSION = 2;
3
+ function isRecord(value) {
4
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
5
+ }
6
+ export function createBundle(languages = {}, updatedAt = Date.now()) {
7
+ return { format: BUNDLE_FORMAT, version: BUNDLE_VERSION, updatedAt, languages };
8
+ }
9
+ export function assertBundle(value) {
10
+ if (!isRecord(value))
11
+ throw new Error('The bundle must be a JSON object.');
12
+ if (value.format !== BUNDLE_FORMAT) {
13
+ throw new Error(`Invalid bundle format. Expected: ${BUNDLE_FORMAT}.`);
14
+ }
15
+ if (value.version !== BUNDLE_VERSION) {
16
+ throw new Error(`Unsupported bundle version: ${String(value.version)}.`);
17
+ }
18
+ if (!Number.isSafeInteger(value.updatedAt) || Number(value.updatedAt) < 0) {
19
+ throw new Error('The bundle must contain a valid numeric updatedAt value.');
20
+ }
21
+ if (!isRecord(value.languages)) {
22
+ throw new Error('The bundle must contain a languages object.');
23
+ }
24
+ for (const [key, language] of Object.entries(value.languages)) {
25
+ if (!isRecord(language) || !isRecord(language.translations)) {
26
+ throw new Error(`Language "${key}" must contain a translations object.`);
27
+ }
28
+ }
29
+ return value;
30
+ }
31
+ export function withUpdatedAt(bundle, updatedAt = Date.now()) {
32
+ const validBundle = assertBundle(bundle);
33
+ return {
34
+ ...validBundle,
35
+ updatedAt: Math.max(updatedAt, validBundle.updatedAt + 1),
36
+ };
37
+ }
38
+ export function parseBundle(text) {
39
+ try {
40
+ return assertBundle(JSON.parse(text));
41
+ }
42
+ catch (error) {
43
+ if (error instanceof SyntaxError) {
44
+ throw new Error('The provided content is not valid JSON.');
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+ export function serializeBundleValue(value) {
50
+ if (typeof value === 'function') {
51
+ return { $type: 'function', source: Function.prototype.toString.call(value) };
52
+ }
53
+ if (Array.isArray(value))
54
+ return value.map(serializeBundleValue);
55
+ if (isRecord(value)) {
56
+ return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [key, serializeBundleValue(nestedValue)]));
57
+ }
58
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
59
+ return value;
60
+ }
61
+ throw new Error(`Unsupported bundle value type: ${typeof value}.`);
62
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ type ExportOptions = {
3
+ input: string;
4
+ output: string;
5
+ config?: string;
6
+ help?: boolean;
7
+ };
8
+ export declare function exportBundle({ input, output, config }: Pick<ExportOptions, 'input' | 'output' | 'config'>): Promise<{
9
+ outputPath: string;
10
+ configOutputPath: string | undefined;
11
+ languageCount: number;
12
+ }>;
13
+ export {};
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createBundle, serializeBundleValue } from '../bundle/index.js';
7
+ import { normalizeProjectConfig, PROJECT_CONFIG_FORMAT, PROJECT_CONFIG_VERSION, } from '../config/index.js';
8
+ const require = createRequire(import.meta.url);
9
+ const projectDirectory = process.cwd();
10
+ function loadTypeScript() {
11
+ try {
12
+ return require('typescript');
13
+ }
14
+ catch {
15
+ throw new Error('i18n-bundle requires TypeScript in the consuming project. Install it with "npm install --save-dev typescript".');
16
+ }
17
+ }
18
+ function parseArguments(argv) {
19
+ const options = {
20
+ input: 'src/shared/i18n/translations/index.ts',
21
+ output: 'i18n.bundle.json',
22
+ };
23
+ for (let index = 0; index < argv.length; index += 1) {
24
+ const argument = argv[index];
25
+ const value = argv[index + 1];
26
+ if (argument === '--input' && value) {
27
+ options.input = value;
28
+ index += 1;
29
+ }
30
+ else if (argument === '--output' && value) {
31
+ options.output = value;
32
+ index += 1;
33
+ }
34
+ else if (argument === '--config' && value) {
35
+ options.config = value;
36
+ index += 1;
37
+ }
38
+ else if (argument === '--help' || argument === '-h') {
39
+ return { ...options, help: true };
40
+ }
41
+ else {
42
+ throw new Error(`Unknown or incomplete argument: ${argument}`);
43
+ }
44
+ }
45
+ return options;
46
+ }
47
+ function printHelp() {
48
+ console.log(`Exports an i18n catalog as a standalone JSON bundle.
49
+
50
+ Usage:
51
+ i18n-bundle --input src/shared/i18n/translations/index.ts --output i18n.bundle.json
52
+
53
+ Options:
54
+ --input Project TypeScript catalog (default: src/shared/i18n/translations/index.ts)
55
+ --output Output JSON file (default: i18n.bundle.json)
56
+ --config Optional i18n.config.json copied beside the output bundle
57
+ --help, -h Show this help`);
58
+ }
59
+ function readTypeScriptConfiguration(ts) {
60
+ const configPath = ts.findConfigFile(projectDirectory, ts.sys.fileExists, 'tsconfig.json');
61
+ if (!configPath)
62
+ return {};
63
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
64
+ if (configFile.error) {
65
+ throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n'));
66
+ }
67
+ return ts.parseJsonConfigFileContent(configFile.config, ts.sys, projectDirectory).options;
68
+ }
69
+ function createTypeScriptLoader(ts, compilerOptions) {
70
+ const cache = new Map();
71
+ function resolveProjectModule(specifier, containingFile) {
72
+ const resolution = ts.resolveModuleName(specifier, containingFile, compilerOptions, ts.sys).resolvedModule;
73
+ if (!resolution || resolution.isExternalLibraryImport)
74
+ return undefined;
75
+ return resolution.resolvedFileName;
76
+ }
77
+ function load(filePath) {
78
+ const absolutePath = path.resolve(filePath);
79
+ const cached = cache.get(absolutePath);
80
+ if (cached)
81
+ return cached.exports;
82
+ const source = fs.readFileSync(absolutePath, 'utf8');
83
+ const compiled = ts.transpileModule(source, {
84
+ compilerOptions: {
85
+ ...compilerOptions,
86
+ module: ts.ModuleKind.CommonJS,
87
+ target: ts.ScriptTarget.ES2022,
88
+ esModuleInterop: true,
89
+ },
90
+ fileName: absolutePath,
91
+ }).outputText;
92
+ const module = { exports: {} };
93
+ cache.set(absolutePath, module);
94
+ const localRequire = (specifier) => {
95
+ const resolved = resolveProjectModule(specifier, absolutePath);
96
+ return resolved ? load(resolved) : require(specifier);
97
+ };
98
+ const execute = new Function('exports', 'require', 'module', '__filename', '__dirname', compiled);
99
+ execute(module.exports, localRequire, module, absolutePath, path.dirname(absolutePath));
100
+ return module.exports;
101
+ }
102
+ return load;
103
+ }
104
+ export async function exportBundle({ input, output, config }) {
105
+ const ts = loadTypeScript();
106
+ const loadProjectTypeScript = createTypeScriptLoader(ts, readTypeScriptConfiguration(ts));
107
+ const translationsIndex = loadProjectTypeScript(path.resolve(projectDirectory, input));
108
+ const langs = translationsIndex.Langs;
109
+ if (!langs || typeof langs !== 'object') {
110
+ throw new Error('The provided catalog must export Langs.');
111
+ }
112
+ const languages = Object.fromEntries(await Promise.all(Object.entries(langs).map(async ([key, info]) => {
113
+ const module = await info.lang();
114
+ const translations = module.default ?? module;
115
+ return [key, {
116
+ name: info.name,
117
+ short: info.short,
118
+ locale: info.locale,
119
+ translations: serializeBundleValue(translations),
120
+ }];
121
+ })));
122
+ const bundle = createBundle(languages);
123
+ const outputPath = path.resolve(projectDirectory, output);
124
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
125
+ fs.writeFileSync(outputPath, `${JSON.stringify(bundle, null, 2)}\n`);
126
+ let configOutputPath;
127
+ if (config) {
128
+ const configValue = JSON.parse(fs.readFileSync(path.resolve(projectDirectory, config), 'utf8'));
129
+ if (configValue.format !== PROJECT_CONFIG_FORMAT || configValue.version !== PROJECT_CONFIG_VERSION) {
130
+ throw new Error(`Invalid project configuration. Expected ${PROJECT_CONFIG_FORMAT} version ${PROJECT_CONFIG_VERSION}.`);
131
+ }
132
+ configOutputPath = path.join(path.dirname(outputPath), 'i18n.config.json');
133
+ fs.writeFileSync(configOutputPath, `${JSON.stringify(normalizeProjectConfig(configValue), null, 2)}\n`);
134
+ }
135
+ return { outputPath, configOutputPath, languageCount: Object.keys(languages).length };
136
+ }
137
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
138
+ try {
139
+ const options = parseArguments(process.argv.slice(2));
140
+ if (options.help) {
141
+ printHelp();
142
+ process.exit(0);
143
+ }
144
+ const result = await exportBundle(options);
145
+ console.log(`i18n bundle created at ${result.outputPath}`);
146
+ if (result.configOutputPath)
147
+ console.log(`i18n project configuration copied to ${result.configOutputPath}`);
148
+ console.log(`${result.languageCount} languages exported.`);
149
+ }
150
+ catch (error) {
151
+ const message = error instanceof Error ? error.message : String(error);
152
+ console.error(`Could not export the i18n bundle: ${message}`);
153
+ process.exit(1);
154
+ }
155
+ }
@@ -0,0 +1,29 @@
1
+ export declare const PROJECT_CONFIG_FORMAT: "wads-i18n-project-config";
2
+ export declare const PROJECT_CONFIG_VERSION: 1;
3
+ export type I18nPathReplacement = string | null | I18nPathReplacer;
4
+ export type I18nPathReplacer = {
5
+ [key: string]: I18nPathReplacement;
6
+ };
7
+ export type I18nLevelImport = {
8
+ path: string;
9
+ valueReplacer?: I18nPathReplacer;
10
+ fullReplacer?: I18nPathReplacer;
11
+ };
12
+ export type I18nProjectConfig = {
13
+ format: typeof PROJECT_CONFIG_FORMAT;
14
+ version: typeof PROJECT_CONFIG_VERSION;
15
+ levelCount: number;
16
+ levelNames: string;
17
+ levelImports: I18nLevelImport[];
18
+ importAliases: Record<string, string>;
19
+ translationsDirectory: string;
20
+ languageFileTemplate: string;
21
+ languageReplacer: Record<string, string>;
22
+ };
23
+ export declare function getDefaultLevelImport(levelIndex: number): I18nLevelImport;
24
+ export declare function createDefaultProjectConfig(): I18nProjectConfig;
25
+ export declare function normalizeProjectConfig(value?: Partial<I18nProjectConfig>): I18nProjectConfig;
26
+ export declare function getLevelName(config: I18nProjectConfig, level: number): string;
27
+ export declare function getLevelImport(config: I18nProjectConfig, levelIndex: number): I18nLevelImport;
28
+ export declare function flattenPathReplacer(replacer: I18nPathReplacer | undefined, parentPath?: string): Record<string, string | null>;
29
+ export declare function resolveImportAlias(path: string, aliases: Record<string, string>): string;
@@ -0,0 +1,109 @@
1
+ export const PROJECT_CONFIG_FORMAT = 'wads-i18n-project-config';
2
+ export const PROJECT_CONFIG_VERSION = 1;
3
+ export function getDefaultLevelImport(levelIndex) {
4
+ if (levelIndex === 0)
5
+ return { path: '@/shared' };
6
+ if (levelIndex === 1)
7
+ return { path: '@/modules/{value}' };
8
+ if (levelIndex === 2)
9
+ return { path: 'features/{value}' };
10
+ return { path: `level-${levelIndex}/{value}` };
11
+ }
12
+ export function createDefaultProjectConfig() {
13
+ return {
14
+ format: PROJECT_CONFIG_FORMAT,
15
+ version: PROJECT_CONFIG_VERSION,
16
+ levelCount: 2,
17
+ levelNames: 'Module, Feature',
18
+ levelImports: [
19
+ getDefaultLevelImport(0),
20
+ getDefaultLevelImport(1),
21
+ getDefaultLevelImport(2),
22
+ ],
23
+ importAliases: { '@/': 'src/' },
24
+ translationsDirectory: 'i18n',
25
+ languageFileTemplate: '{language}.ts',
26
+ languageReplacer: {},
27
+ };
28
+ }
29
+ function isRecord(value) {
30
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
31
+ }
32
+ function normalizeReplacer(value) {
33
+ if (!isRecord(value))
34
+ return {};
35
+ const result = {};
36
+ Object.entries(value).forEach(([key, replacement]) => {
37
+ if (replacement === null || typeof replacement === 'string')
38
+ result[key] = replacement;
39
+ else if (isRecord(replacement))
40
+ result[key] = normalizeReplacer(replacement);
41
+ });
42
+ return result;
43
+ }
44
+ function normalizeStringRecord(value) {
45
+ if (!isRecord(value))
46
+ return {};
47
+ return Object.fromEntries(Object.entries(value)
48
+ .filter((entry) => typeof entry[1] === 'string')
49
+ .map(([key, item]) => [key, item.trim()]));
50
+ }
51
+ export function normalizeProjectConfig(value = {}) {
52
+ const defaults = createDefaultProjectConfig();
53
+ const levelCount = Math.max(0, Number.parseInt(String(value.levelCount ?? defaults.levelCount), 10) || 0);
54
+ const inputImports = Array.isArray(value.levelImports) ? value.levelImports : [];
55
+ return {
56
+ format: PROJECT_CONFIG_FORMAT,
57
+ version: PROJECT_CONFIG_VERSION,
58
+ levelCount,
59
+ levelNames: typeof value.levelNames === 'string' ? value.levelNames : defaults.levelNames,
60
+ levelImports: Array.from({ length: levelCount + 1 }, (_, index) => {
61
+ const input = isRecord(inputImports[index]) ? inputImports[index] : {};
62
+ const fallback = getDefaultLevelImport(index);
63
+ return {
64
+ path: typeof input.path === 'string' && input.path.trim() ? input.path.trim() : fallback.path,
65
+ valueReplacer: normalizeReplacer(input.valueReplacer),
66
+ fullReplacer: normalizeReplacer(input.fullReplacer),
67
+ };
68
+ }),
69
+ importAliases: {
70
+ ...defaults.importAliases,
71
+ ...normalizeStringRecord(value.importAliases),
72
+ },
73
+ translationsDirectory: typeof value.translationsDirectory === 'string' && value.translationsDirectory.trim()
74
+ ? value.translationsDirectory.trim()
75
+ : defaults.translationsDirectory,
76
+ languageFileTemplate: typeof value.languageFileTemplate === 'string' && value.languageFileTemplate.trim()
77
+ ? value.languageFileTemplate.trim()
78
+ : defaults.languageFileTemplate,
79
+ languageReplacer: normalizeStringRecord(value.languageReplacer),
80
+ };
81
+ }
82
+ export function getLevelName(config, level) {
83
+ if (level === 0)
84
+ return 'Root';
85
+ const names = config.levelNames.split(',').map((name) => name.trim()).filter(Boolean);
86
+ return names[level - 1] || `Level ${level}`;
87
+ }
88
+ export function getLevelImport(config, levelIndex) {
89
+ return config.levelImports[levelIndex] || getDefaultLevelImport(levelIndex);
90
+ }
91
+ export function flattenPathReplacer(replacer, parentPath = '') {
92
+ const flattened = {};
93
+ Object.entries(replacer || {}).forEach(([key, value]) => {
94
+ const fullKey = parentPath ? `${parentPath}.${key}` : key;
95
+ if (value === null || typeof value === 'string')
96
+ flattened[fullKey] = value;
97
+ else
98
+ Object.assign(flattened, flattenPathReplacer(value, fullKey));
99
+ });
100
+ return flattened;
101
+ }
102
+ export function resolveImportAlias(path, aliases) {
103
+ const alias = Object.keys(aliases)
104
+ .filter((candidate) => path.startsWith(candidate))
105
+ .sort((left, right) => right.length - left.length)[0];
106
+ if (!alias)
107
+ return path;
108
+ return `${aliases[alias]}${path.slice(alias.length)}`;
109
+ }
@@ -0,0 +1,2 @@
1
+ export { loadLanguage } from './language.js';
2
+ export type { AsyncTranslation, AvailableLangs, DeepReadonly, LangInfo, LoadedLangInfo, Translation, TranslationItems, TranslationRender, TranslationResult, TranslationTokenRender, TranslationTokens, TranslationTokensRender, TranslationWithParams, } from './types.js';
@@ -0,0 +1 @@
1
+ export { loadLanguage } from './language.js';
@@ -0,0 +1,2 @@
1
+ import type { AvailableLangs, LoadedLangInfo, Translation } from './types.js';
2
+ export declare function loadLanguage<A extends string, T extends Translation>(availableLangs: AvailableLangs<A, T>, defaultLang: A, language?: A | string): Promise<LoadedLangInfo<T>>;
@@ -0,0 +1,31 @@
1
+ function detectLanguage() {
2
+ if (typeof navigator !== 'undefined' && typeof navigator.language === 'string')
3
+ return navigator.language;
4
+ if (typeof Intl !== 'undefined')
5
+ return Intl.DateTimeFormat().resolvedOptions().locale;
6
+ return undefined;
7
+ }
8
+ export async function loadLanguage(availableLangs, defaultLang, language) {
9
+ if (!language || !availableLangs[language]) {
10
+ const browserLang = (detectLanguage() || '').replace('-', '').toLowerCase();
11
+ const languageKeys = Object.keys(availableLangs).map((key) => key.toLowerCase());
12
+ language = languageKeys.find((key) => key === browserLang)
13
+ || languageKeys.find((key) => browserLang.startsWith(key))
14
+ || defaultLang;
15
+ }
16
+ const info = availableLangs[language];
17
+ const lang = typeof info.lang === 'function'
18
+ ? await info.lang().then(({ default: loadedLang }) => loadedLang)
19
+ : info.lang;
20
+ return { ...info, lang: deepFreeze(lang) };
21
+ }
22
+ function deepFreeze(value) {
23
+ Object.freeze(value);
24
+ Object.getOwnPropertyNames(value).forEach((property) => {
25
+ const nestedValue = value[property];
26
+ if (nestedValue && (typeof nestedValue === 'object' || typeof nestedValue === 'function') && !Object.isFrozen(nestedValue)) {
27
+ deepFreeze(nestedValue);
28
+ }
29
+ });
30
+ return value;
31
+ }
@@ -0,0 +1,27 @@
1
+ export type TranslationResult<T> = unknown;
2
+ export type TranslationTokens<T extends string> = Record<T, string>;
3
+ export type TranslationTokenRender<T> = (text: string, token?: T) => TranslationResult<T>;
4
+ export type TranslationTokensRender<T extends string> = {
5
+ default?: TranslationTokenRender<T>;
6
+ } & Record<T, TranslationTokenRender<T>>;
7
+ export type TranslationRender<Tokens extends string = string> = (customTokens?: TranslationTokens<Tokens>, customRenders?: TranslationTokensRender<Tokens>) => TranslationResult<Tokens>;
8
+ export type TranslationItems = string[];
9
+ export type TranslationWithParams = (...args: any[]) => string;
10
+ export interface Translation {
11
+ [key: string]: string | Translation | Translation[] | TranslationItems | TranslationWithParams | TranslationRender<any>;
12
+ }
13
+ export type ModuleReturn<T extends Translation> = {
14
+ default: T;
15
+ };
16
+ export type AsyncTranslation<T extends Translation> = () => Promise<ModuleReturn<T>>;
17
+ export type LangInfo<T extends Translation, LT = T | AsyncTranslation<T>> = {
18
+ lang: LT;
19
+ locale: string;
20
+ name: string;
21
+ short: string;
22
+ };
23
+ export type AvailableLangs<A extends string, T extends Translation> = Record<A, LangInfo<T>>;
24
+ export type DeepReadonly<T> = T extends TranslationWithParams | TranslationRender ? T : {
25
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
26
+ };
27
+ export type LoadedLangInfo<T extends Translation> = LangInfo<T, DeepReadonly<T>>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ # Bundle and translation-review workflow
2
+
3
+ An i18n bundle is a portable JSON snapshot of every configured language. It contains language metadata, the full translation tree and an `updatedAt` timestamp without exposing the repository's TypeScript module layout.
4
+
5
+ ## What the bundle solves
6
+
7
+ Large applications often distribute translations across root, module and feature directories. That structure is useful for developers but inconvenient for translators and reviewers. A bundle provides one transport format that can be:
8
+
9
+ - opened in `@wads.dev/i18n-editor`;
10
+ - sent to an internal localization team or external translator;
11
+ - archived as a review artifact;
12
+ - compared with a newer project export;
13
+ - returned to developers without manually editing many source files.
14
+
15
+ Functions are represented as safe descriptors. Tools can inspect their signatures and bodies without executing application code.
16
+
17
+ ## Export from the project
18
+
19
+ ```sh
20
+ npx i18n-bundle \
21
+ --input src/i18n/index.ts \
22
+ --config i18n.config.json \
23
+ --output i18n.bundle.json
24
+ ```
25
+
26
+ The catalog module must export `Langs`. When `--config` is supplied, the normalized configuration is copied beside the bundle for the Editor.
27
+
28
+ ## Review and edit
29
+
30
+ The Editor loads the bundle as one in-memory object and uses `i18n.config.json` to present structural levels such as modules and features. Reviewers can navigate all languages together, compare values and edit text without understanding the source repository.
31
+
32
+ The intended collaboration cycle is:
33
+
34
+ 1. A developer exports the current bundle and configuration.
35
+ 2. The translation team reviews or changes the portable bundle.
36
+ 3. The reviewed bundle returns to development.
37
+ 4. Editor/CLI tooling resolves every key through the configuration and regenerates the TypeScript translation tree.
38
+ 5. Developers inspect the generated Git diff, run validation and merge normally.
39
+
40
+ ## Why regenerate instead of patching files
41
+
42
+ Deterministic regeneration avoids teaching the importer every possible formatting style or trying to perform fragile textual edits. Translation files should contain only generated translation structures so they can be recreated from the reviewed bundle.
43
+
44
+ Git remains the safety layer:
45
+
46
+ - unexpected moves are visible;
47
+ - removed or duplicated keys are reviewable;
48
+ - formatting and structural changes are explicit;
49
+ - typecheck and tests validate the generated result;
50
+ - reverting a problematic import remains straightforward.
51
+
52
+ ## Current alpha status
53
+
54
+ Available now:
55
+
56
+ - TypeScript catalog to portable bundle export;
57
+ - normalized project configuration copied beside the bundle;
58
+ - Editor visualization, grouping and in-memory text/key editing;
59
+ - export-plan preview showing the files implied by the configuration.
60
+
61
+ Still to be implemented:
62
+
63
+ - importing a reviewed bundle back into generated TypeScript files;
64
+ - deterministic file templates for contracts, indexes and locales;
65
+ - ZIP export/import and conflict-resolution workflows.
66
+
67
+ The documentation distinguishes this target workflow from the features already shipped so teams can adopt the bundle format without assuming source regeneration is complete.
@@ -0,0 +1,190 @@
1
+ # Project configuration
2
+
3
+ `i18n.config.json` is the portable description of how a translation object maps to a repository. Keep it at the project root and commit it with the source code.
4
+
5
+ The application runtime does not read this file. Its primary consumers are:
6
+
7
+ - AI agents that need to locate contracts, modules, features and locale files safely;
8
+ - `@wads.dev/i18n-editor`, which groups translations and previews regenerated files;
9
+ - CLI automation that converts between a portable bundle and the project's source layout.
10
+
11
+ ## Complete example
12
+
13
+ ```json
14
+ {
15
+ "format": "wads-i18n-project-config",
16
+ "version": 1,
17
+ "levelCount": 2,
18
+ "levelNames": "Module, Feature",
19
+ "levelImports": [
20
+ { "path": "@/shared" },
21
+ {
22
+ "path": "@/modules/{value}",
23
+ "fullReplacer": {
24
+ "commons": null,
25
+ "designSystemPreview": "@design/preview",
26
+ "firebase": "@/shared/config/firebase"
27
+ }
28
+ },
29
+ {
30
+ "path": "features/{value}",
31
+ "valueReplacer": {
32
+ "logged": {
33
+ "hubNews": "hub-new",
34
+ "openStrategy": "open-strategy"
35
+ }
36
+ },
37
+ "fullReplacer": {
38
+ "logged.navigation": null
39
+ }
40
+ }
41
+ ],
42
+ "importAliases": {
43
+ "@design/": "src/shared/design-system/",
44
+ "@/": "src/"
45
+ },
46
+ "translationsDirectory": "i18n",
47
+ "languageFileTemplate": "{language}.ts",
48
+ "languageReplacer": {
49
+ "pt": "ptBR"
50
+ }
51
+ }
52
+ ```
53
+
54
+ JSON does not support comments. Keep explanations in documentation rather than adding comments to the real configuration file.
55
+
56
+ ## Top-level fields
57
+
58
+ | Field | Type | Meaning |
59
+ | --- | --- | --- |
60
+ | `format` | string | Contract identifier. Currently `wads-i18n-project-config`. |
61
+ | `version` | number | Configuration schema version. Currently `1`. |
62
+ | `levelCount` | number | Structural key levels, excluding root. |
63
+ | `levelNames` | string | Comma-separated display names used by tools. Missing entries become `Level N`. |
64
+ | `levelImports` | array | Root entry plus one entry for each structural level. Its length is `levelCount + 1`. |
65
+ | `importAliases` | object | Maps path prefixes to project-relative directories. |
66
+ | `translationsDirectory` | string | Directory appended to every resolved translation owner. |
67
+ | `languageFileTemplate` | string | Locale filename template with `{language}`. |
68
+ | `languageReplacer` | object | Maps catalog language keys to actual filename values. |
69
+
70
+ ## Root and structural levels
71
+
72
+ `levelImports[0]` is always the root translation location. It consumes no segment of the translation key.
73
+
74
+ Entries `1...levelCount` consume one segment each. Given the key:
75
+
76
+ ```text
77
+ shell.authentication.login.title
78
+ ```
79
+
80
+ and two configured levels:
81
+
82
+ - `shell` is level 1 (`Module`);
83
+ - `authentication` is level 2 (`Feature`);
84
+ - `login.title` remains the key inside the feature files.
85
+
86
+ An aliased path is rooted at the project. An unaliased path is appended to the previously resolved level. Therefore:
87
+
88
+ ```json
89
+ [
90
+ { "path": "@/shared" },
91
+ { "path": "@/modules/{value}" },
92
+ { "path": "features/{value}" }
93
+ ]
94
+ ```
95
+
96
+ resolves the example to:
97
+
98
+ ```text
99
+ src/modules/shell/features/authentication/i18n/{language}.ts
100
+ ```
101
+
102
+ ## Value replacer
103
+
104
+ `valueReplacer` changes only the value inserted into `{value}`. Use it when object keys and existing folder names differ by casing, separators or legacy naming.
105
+
106
+ These forms are equivalent:
107
+
108
+ ```json
109
+ { "logged.hubNews": "hub-new" }
110
+ ```
111
+
112
+ ```json
113
+ { "logged": { "hubNews": "hub-new" } }
114
+ ```
115
+
116
+ The full object path makes the rule unambiguous even when two modules contain features with the same name.
117
+
118
+ ## Full replacer
119
+
120
+ `fullReplacer` overrides normal resolution for a complete object path.
121
+
122
+ A string redirects the subtree:
123
+
124
+ ```json
125
+ { "designSystemPreview": "@design/preview" }
126
+ ```
127
+
128
+ `null` stops descending and keeps the subtree in its parent translation files:
129
+
130
+ ```json
131
+ { "logged.navigation": null }
132
+ ```
133
+
134
+ This allows a nested object to remain in `src/modules/logged/i18n` even though the project normally treats the second segment as a feature.
135
+
136
+ Like `valueReplacer`, it accepts dotted keys and nested objects.
137
+
138
+ ## Import aliases
139
+
140
+ Aliases map configuration paths to project-relative directories. The longest matching prefix wins.
141
+
142
+ ```json
143
+ {
144
+ "@design/": "src/shared/design-system/",
145
+ "@/": "src/"
146
+ }
147
+ ```
148
+
149
+ With this definition, use `@/modules/{value}`, not `@/src/modules/{value}`. Adding `src` to both sides would incorrectly produce `src/src/modules/...`.
150
+
151
+ ## Language filenames
152
+
153
+ `languageFileTemplate` controls the generated filename:
154
+
155
+ ```json
156
+ { "languageFileTemplate": "{language}.ts" }
157
+ ```
158
+
159
+ When a catalog key differs from the existing filename, use `languageReplacer`:
160
+
161
+ ```json
162
+ { "languageReplacer": { "pt": "ptBR" } }
163
+ ```
164
+
165
+ The catalog language `pt` then generates `ptBR.ts`.
166
+
167
+ ## Generated destinations
168
+
169
+ For every resolved owner, the Editor export plan currently includes:
170
+
171
+ ```text
172
+ i18n/base.ts
173
+ i18n/index.ts
174
+ i18n/{language}.ts
175
+ ```
176
+
177
+ The preview updates immediately when levels, paths, aliases or replacers change.
178
+
179
+ ## TypeScript API
180
+
181
+ Tooling can consume the same normalized contract:
182
+
183
+ ```ts
184
+ import {
185
+ normalizeProjectConfig,
186
+ type I18nProjectConfig,
187
+ } from '@wads.dev/i18n-ts/config'
188
+ ```
189
+
190
+ Always normalize external JSON before resolving paths.
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@wads.dev/i18n-ts",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "Dependency-free typed i18n contracts, bundle tools and language loading.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Wads.dev",
8
+ "url": "https://github.com/wads-dev"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/wads-dev/i18n-ts.git"
13
+ },
14
+ "homepage": "https://github.com/wads-dev/i18n-ts#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/wads-dev/i18n-ts/issues"
17
+ },
18
+ "keywords": [
19
+ "i18n",
20
+ "internationalization",
21
+ "typescript",
22
+ "translations"
23
+ ],
24
+ "type": "module",
25
+ "files": [
26
+ "AI",
27
+ "docs",
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "scripts": {
36
+ "build": "node ./scripts/clean-dist.mjs && tsc --project tsconfig.build.json",
37
+ "check": "tsc --project tsconfig.check.json --noEmit",
38
+ "prepack": "npm run check && npm run build"
39
+ },
40
+ "bin": {
41
+ "i18n-bundle": "./dist/cli/exportBundle.js"
42
+ },
43
+ "exports": {
44
+ ".": {
45
+ "types": "./dist/runtime/index.d.ts",
46
+ "import": "./dist/runtime/index.js"
47
+ },
48
+ "./bundle": {
49
+ "types": "./dist/bundle/index.d.ts",
50
+ "import": "./dist/bundle/index.js"
51
+ },
52
+ "./config": {
53
+ "types": "./dist/config/index.d.ts",
54
+ "import": "./dist/config/index.js"
55
+ }
56
+ },
57
+ "peerDependencies": {
58
+ "typescript": ">=5"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "typescript": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^24.0.0",
67
+ "typescript": "^6.0.3"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ }
72
+ }