@apappas1129/ngx-fluent-extract 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @apappas1129/ngx-fluent-extract
2
+
3
+ A report-only extraction CLI for [ngx-fluent](https://github.com/apappas1129/ngx-fluent). It diffs the translation keys used in your Angular app's code against each locale's `.ftl` file, and tells you what's **missing** (used in code, not defined) and **orphaned** (defined, never used).
4
+
5
+ It never writes to `.ftl` files. Fluent messages can carry variants, attributes, and term references that a generated stub can't meaningfully guess — so this tool only reports, it doesn't scaffold.
6
+
7
+ ## When you'd need this
8
+
9
+ Nothing in ngx-fluent stops your app from compiling or running if a translation key is missing — the `fluent` pipe just falls back to showing the raw key string (`'welcome-user'` instead of "Welcome!"). That's silent in dev and in code review; the first anyone notices is usually a user seeing a raw key in production, or a translator asking why a locale looks half-finished. Reach for this CLI when:
10
+
11
+ - **You're about to add a new locale.** Point the config at the new `.ftl` file and see exactly which keys it's missing before you ship it, instead of finding out from a user.
12
+ - **You just refactored or deleted a component.** The `.ftl` entries it used don't get cleaned up automatically — `orphaned` tells you what's now dead weight in your translation files.
13
+ - **You want a CI check on PRs that touch templates or translations.** A key typo'd in a template, or added in code but never given a translation, exits non-zero — catch it before merge instead of after a user reports it.
14
+ - **A `.ftl` file came back from a translator or survived a merge conflict.** Malformed Fluent syntax fails silently at runtime (the bundle just drops the broken entry); this CLI's `parseError` surfaces it as a build-breaking problem, not a runtime mystery.
15
+
16
+ If none of that applies — small app, one locale, translations always land alongside the code that uses them — you probably don't need this yet.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install --save-dev @apappas1129/ngx-fluent-extract
22
+ ```
23
+
24
+ Requires TypeScript as a peer dependency (already present in any Angular project).
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ npx ngx-fluent-extract [scanRoot] [options]
30
+ ```
31
+
32
+ - `scanRoot` — directory to scan from. Defaults to the current directory (like `code .` / `code <path>`).
33
+ - `--config <path>` — path to the extraction config. Defaults to `<scanRoot>/ngx-fluent-extract.config.json`.
34
+ - `--project <path>` — path to a `tsconfig.json`. Defaults to auto-discovery from `scanRoot` (following `references` one level).
35
+ - `--json` — print the report as JSON instead of human-readable text.
36
+ - `--help` — show usage.
37
+
38
+ Exit code is non-zero only when a locale has **missing** keys or a **parse error**. Orphaned keys alone don't fail the run — they're hygiene, not a defect users will see.
39
+
40
+ ## Config
41
+
42
+ `ngx-fluent-extract.config.json`, at the scan root:
43
+
44
+ ```json
45
+ {
46
+ "locales": {
47
+ "en": "public/i18n/en.ftl",
48
+ "sv": "public/i18n/sv.ftl"
49
+ }
50
+ }
51
+ ```
52
+
53
+ Paths are resolved relative to the config file's own directory. There's no source-glob field — source files are discovered from the resolved `tsconfig.json`'s file list plus each `@Component`'s `templateUrl`/inline `template`, not by pattern matching.
54
+
55
+ ## What it detects
56
+
57
+ - `'key' | fluent` pipe usage in templates (inline or external, via `templateUrl`)
58
+ - `NgxFluentService.translate('key', ...)` calls in TypeScript, matched type-aware (via the TypeScript Compiler API) rather than by name alone, so an unrelated `.translate()` method on another class isn't mistaken for this library's
59
+
60
+ A key that isn't a string literal (a variable, a function call) can't be resolved statically. Those surface as **dynamic key warnings** — file and line — rather than being silently dropped, so they don't corrupt the missing/orphaned counts.
61
+
62
+ ## Out of scope for v1
63
+
64
+ - **Message attributes** (`login-button.title`) — `NgxFluentService.translate()` doesn't expose attribute lookup at all yet, so there's nothing in code for an attribute-aware report to check against.
65
+ - **Fluent terms** (`-brand-name`) — terms are only referenced from within other messages, never from code. Checking whether a term is used anywhere in a `.ftl` file is a different job (dead-code-within-`.ftl`) from this tool's code-vs-translation-file diff.
66
+
67
+ ## Why a standalone CLI, not an `ng` builder
68
+
69
+ An Angular builder (`ng run ... :extract-i18n`-style) would need `@angular-devkit/architect` and an `angular.json` target for what's fundamentally a diagnostic side-channel, not a build step. A plain CLI runs anywhere — this repo's own example apps, any CI pipeline — without that coupling. It ships as its own package rather than bundled into `@apappas1129/ngx-fluent` because that library is built with `ng-packagr` for Angular consumption, which can't produce a runnable `bin` script.
70
+
71
+ A thin Angular builder that wraps this same CLI is a good scope for a community PR if there's demand — issues and PRs welcome.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { formatJson, formatText, run } from './index.js';
4
+ function printHelp() {
5
+ console.log(`Usage: ngx-fluent-extract [scanRoot] [options]
6
+
7
+ Diffs translation keys used in an Angular app's code against each locale's .ftl file.
8
+ Report-only — never writes to .ftl files.
9
+
10
+ Options:
11
+ --config <path> Path to the extraction config (default: <scanRoot>/ngx-fluent-extract.config.json)
12
+ --project <path> Path to a tsconfig.json (default: auto-discovered from scanRoot)
13
+ --json Print the report as JSON instead of human-readable text
14
+ --help Show this help message
15
+ `);
16
+ }
17
+ const { values, positionals } = parseArgs({
18
+ args: process.argv.slice(2),
19
+ allowPositionals: true,
20
+ options: {
21
+ config: { type: 'string' },
22
+ project: { type: 'string' },
23
+ json: { type: 'boolean', default: false },
24
+ help: { type: 'boolean', default: false },
25
+ },
26
+ });
27
+ if (values.help) {
28
+ printHelp();
29
+ process.exit(0);
30
+ }
31
+ const scanRoot = positionals[0] ?? process.cwd();
32
+ try {
33
+ const { report, exitCode } = run({ scanRoot, configPath: values.config, projectPath: values.project });
34
+ console.log(values.json ? formatJson(report) : formatText(report));
35
+ process.exitCode = exitCode;
36
+ }
37
+ catch (error) {
38
+ console.error(error.message);
39
+ process.exitCode = 1;
40
+ }
@@ -0,0 +1,4 @@
1
+ import type { ExtractConfig } from './types.js';
2
+ export declare const DEFAULT_CONFIG_FILENAME = "ngx-fluent-extract.config.json";
3
+ /** Reads and validates the config, resolving each locale path relative to the config file's directory. */
4
+ export declare function loadConfig(configPath: string): ExtractConfig;
package/dist/config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, isAbsolute, resolve } from 'node:path';
3
+ export const DEFAULT_CONFIG_FILENAME = 'ngx-fluent-extract.config.json';
4
+ /** Reads and validates the config, resolving each locale path relative to the config file's directory. */
5
+ export function loadConfig(configPath) {
6
+ let raw;
7
+ try {
8
+ raw = readFileSync(configPath, 'utf8');
9
+ }
10
+ catch {
11
+ throw new Error(`Config file not found: ${configPath}`);
12
+ }
13
+ let parsed;
14
+ try {
15
+ parsed = JSON.parse(raw);
16
+ }
17
+ catch (error) {
18
+ throw new Error(`Config file is not valid JSON: ${configPath}\n${error.message}`);
19
+ }
20
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
21
+ throw new Error(`Config file must be a JSON object with a "locales" field: ${configPath}`);
22
+ }
23
+ const locales = parsed['locales'];
24
+ if (typeof locales !== 'object' || locales === null || Array.isArray(locales)) {
25
+ throw new Error(`Config file's "locales" field must be an object mapping locale -> .ftl path: ${configPath}`);
26
+ }
27
+ const entries = Object.entries(locales);
28
+ if (entries.length === 0) {
29
+ throw new Error(`Config file's "locales" field must not be empty: ${configPath}`);
30
+ }
31
+ const configDir = dirname(configPath);
32
+ const resolved = {};
33
+ for (const [locale, path] of entries) {
34
+ if (typeof path !== 'string') {
35
+ throw new Error(`Config file's "locales.${locale}" must be a string path: ${configPath}`);
36
+ }
37
+ resolved[locale] = isAbsolute(path) ? path : resolve(configDir, path);
38
+ }
39
+ return { locales: resolved };
40
+ }
@@ -0,0 +1,7 @@
1
+ export interface FtlScanResult {
2
+ messageIds: string[];
3
+ /** Human-readable summary of the first parse error found, or null if the file parsed cleanly. */
4
+ parseError: string | null;
5
+ }
6
+ /** Parses a locale's .ftl file, collecting top-level message identifiers and any Junk (parse error) entries. */
7
+ export declare function scanFtlFile(path: string): FtlScanResult;
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { FluentParser } from '@fluent/syntax';
3
+ const parser = new FluentParser();
4
+ /** Parses a locale's .ftl file, collecting top-level message identifiers and any Junk (parse error) entries. */
5
+ export function scanFtlFile(path) {
6
+ let content;
7
+ try {
8
+ content = readFileSync(path, 'utf8');
9
+ }
10
+ catch {
11
+ return { messageIds: [], parseError: `File not found: ${path}` };
12
+ }
13
+ const resource = parser.parse(content);
14
+ const messageIds = [];
15
+ const junkMessages = [];
16
+ for (const entry of resource.body) {
17
+ if (entry.type === 'Message') {
18
+ messageIds.push(entry.id.name);
19
+ }
20
+ else if (entry.type === 'Junk') {
21
+ for (const annotation of entry.annotations) {
22
+ junkMessages.push(annotation.message);
23
+ }
24
+ }
25
+ }
26
+ return {
27
+ messageIds,
28
+ parseError: junkMessages.length > 0 ? junkMessages.join('; ') : null,
29
+ };
30
+ }
@@ -0,0 +1,13 @@
1
+ import type { Report } from './types.js';
2
+ export interface RunOptions {
3
+ scanRoot: string;
4
+ configPath?: string;
5
+ projectPath?: string;
6
+ }
7
+ export interface RunResult {
8
+ report: Report;
9
+ exitCode: number;
10
+ }
11
+ export declare function run(options: RunOptions): RunResult;
12
+ export { formatJson, formatText } from './report.js';
13
+ export type { Report } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ import { resolve } from 'node:path';
2
+ import { DEFAULT_CONFIG_FILENAME, loadConfig } from './config.js';
3
+ import { scanFtlFile } from './ftl-scan.js';
4
+ import { buildReport, hasFailures } from './report.js';
5
+ import { scanTemplates } from './template-scan.js';
6
+ import { loadProgram, scanProgram } from './ts-scan.js';
7
+ export function run(options) {
8
+ const scanRoot = resolve(options.scanRoot);
9
+ const configPath = options.configPath
10
+ ? resolve(scanRoot, options.configPath)
11
+ : resolve(scanRoot, DEFAULT_CONFIG_FILENAME);
12
+ const config = loadConfig(configPath);
13
+ const program = loadProgram(scanRoot, options.projectPath);
14
+ const tsResult = scanProgram(program);
15
+ const templateResult = scanTemplates(tsResult.componentTemplates);
16
+ const keyUsages = [...tsResult.keyUsages, ...templateResult.keyUsages];
17
+ const dynamicKeyWarnings = [...tsResult.dynamicKeyWarnings, ...templateResult.dynamicKeyWarnings];
18
+ const localeMessages = {};
19
+ for (const [locale, path] of Object.entries(config.locales)) {
20
+ localeMessages[locale] = scanFtlFile(path);
21
+ }
22
+ const report = buildReport(keyUsages, dynamicKeyWarnings, localeMessages);
23
+ return { report, exitCode: hasFailures(report) ? 1 : 0 };
24
+ }
25
+ export { formatJson, formatText } from './report.js';
@@ -0,0 +1,8 @@
1
+ import type { DynamicKeyWarning, KeyUsage, Report } from './types.js';
2
+ export declare function buildReport(keyUsages: KeyUsage[], dynamicKeyWarnings: DynamicKeyWarning[], localeMessages: Record<string, {
3
+ messageIds: string[];
4
+ parseError: string | null;
5
+ }>): Report;
6
+ export declare function hasFailures(report: Report): boolean;
7
+ export declare function formatText(report: Report): string;
8
+ export declare function formatJson(report: Report): string;
package/dist/report.js ADDED
@@ -0,0 +1,56 @@
1
+ export function buildReport(keyUsages, dynamicKeyWarnings, localeMessages) {
2
+ const usedKeys = new Set(keyUsages.map((usage) => usage.key));
3
+ const locales = {};
4
+ for (const [locale, { messageIds, parseError }] of Object.entries(localeMessages)) {
5
+ if (parseError) {
6
+ // A partially-parsed file can't produce trustworthy missing/orphaned counts.
7
+ locales[locale] = { missing: [], orphaned: [], parseError };
8
+ continue;
9
+ }
10
+ const definedKeys = new Set(messageIds);
11
+ locales[locale] = {
12
+ missing: [...usedKeys].filter((key) => !definedKeys.has(key)).sort(),
13
+ orphaned: messageIds.filter((key) => !usedKeys.has(key)).sort(),
14
+ parseError: null,
15
+ };
16
+ }
17
+ return { locales, dynamicKeyWarnings };
18
+ }
19
+ export function hasFailures(report) {
20
+ return Object.values(report.locales).some((locale) => locale.missing.length > 0 || locale.parseError !== null);
21
+ }
22
+ export function formatText(report) {
23
+ const lines = [];
24
+ for (const [locale, localeReport] of Object.entries(report.locales)) {
25
+ lines.push(`${locale}:`);
26
+ if (localeReport.parseError) {
27
+ lines.push(` parse error: ${localeReport.parseError}`);
28
+ lines.push(' (missing/orphaned skipped for this locale until the parse error is fixed)');
29
+ continue;
30
+ }
31
+ if (localeReport.missing.length === 0 && localeReport.orphaned.length === 0) {
32
+ lines.push(' ok');
33
+ continue;
34
+ }
35
+ if (localeReport.missing.length > 0) {
36
+ lines.push(` missing (${localeReport.missing.length}):`);
37
+ for (const key of localeReport.missing)
38
+ lines.push(` - ${key}`);
39
+ }
40
+ if (localeReport.orphaned.length > 0) {
41
+ lines.push(` orphaned (${localeReport.orphaned.length}):`);
42
+ for (const key of localeReport.orphaned)
43
+ lines.push(` - ${key}`);
44
+ }
45
+ }
46
+ if (report.dynamicKeyWarnings.length > 0) {
47
+ lines.push('');
48
+ lines.push(`dynamic keys, could not verify (${report.dynamicKeyWarnings.length}):`);
49
+ for (const warning of report.dynamicKeyWarnings)
50
+ lines.push(` - ${warning.file}:${warning.line}`);
51
+ }
52
+ return lines.join('\n');
53
+ }
54
+ export function formatJson(report) {
55
+ return JSON.stringify(report, null, 2);
56
+ }
@@ -0,0 +1,10 @@
1
+ import type { ComponentTemplate } from './ts-scan.js';
2
+ import type { DynamicKeyWarning, KeyUsage } from './types.js';
3
+ export declare function scanTemplateText(text: string, file: string): {
4
+ keyUsages: KeyUsage[];
5
+ dynamicKeyWarnings: DynamicKeyWarning[];
6
+ };
7
+ export declare function scanTemplates(templates: ComponentTemplate[]): {
8
+ keyUsages: KeyUsage[];
9
+ dynamicKeyWarnings: DynamicKeyWarning[];
10
+ };
@@ -0,0 +1,42 @@
1
+ // ponytail: regex-based, not a full Angular template parser. Handles the common
2
+ // `'key' | fluent` / `{{ someExpr | fluent }}` shapes; upgrade to @angular/compiler's
3
+ // parseTemplate if a real-world template expression shape slips past this.
4
+ const LITERAL_KEY_RE = /(['"])((?:\\.|(?!\1).)*)\1\s*\|\s*fluent\b/g;
5
+ const PIPE_USAGE_RE = /\|\s*fluent\b/g;
6
+ function lineAt(text, index) {
7
+ let line = 1;
8
+ for (let i = 0; i < index; i++) {
9
+ if (text.charCodeAt(i) === 10 /* \n */)
10
+ line++;
11
+ }
12
+ return line;
13
+ }
14
+ export function scanTemplateText(text, file) {
15
+ const keyUsages = [];
16
+ const dynamicKeyWarnings = [];
17
+ const literalEnds = new Set();
18
+ for (const match of text.matchAll(LITERAL_KEY_RE)) {
19
+ const index = match.index ?? 0;
20
+ const end = index + match[0].length;
21
+ literalEnds.add(end);
22
+ keyUsages.push({ key: match[2].replace(/\\(.)/g, '$1'), file, line: lineAt(text, index) });
23
+ }
24
+ for (const match of text.matchAll(PIPE_USAGE_RE)) {
25
+ const index = match.index ?? 0;
26
+ const end = index + match[0].length;
27
+ if (!literalEnds.has(end)) {
28
+ dynamicKeyWarnings.push({ file, line: lineAt(text, index) });
29
+ }
30
+ }
31
+ return { keyUsages, dynamicKeyWarnings };
32
+ }
33
+ export function scanTemplates(templates) {
34
+ const keyUsages = [];
35
+ const dynamicKeyWarnings = [];
36
+ for (const template of templates) {
37
+ const result = scanTemplateText(template.text, template.file);
38
+ keyUsages.push(...result.keyUsages);
39
+ dynamicKeyWarnings.push(...result.dynamicKeyWarnings);
40
+ }
41
+ return { keyUsages, dynamicKeyWarnings };
42
+ }
@@ -0,0 +1,16 @@
1
+ import ts from 'typescript';
2
+ import type { DynamicKeyWarning, KeyUsage } from './types.js';
3
+ export interface ComponentTemplate {
4
+ text: string;
5
+ /** Path used for reporting line numbers — the external template file, or the component's own file for inline templates. */
6
+ file: string;
7
+ }
8
+ export interface TsScanResult {
9
+ keyUsages: KeyUsage[];
10
+ dynamicKeyWarnings: DynamicKeyWarning[];
11
+ componentTemplates: ComponentTemplate[];
12
+ }
13
+ /** Loads a ts.Program from a tsconfig, following one level of project references to gather all root files. */
14
+ export declare function loadProgram(scanRoot: string, projectPath?: string): ts.Program;
15
+ /** Scans a ts.Program for NgxFluentService.translate() calls and @Component template sources. */
16
+ export declare function scanProgram(program: ts.Program): TsScanResult;
@@ -0,0 +1,109 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import ts from 'typescript';
4
+ const NGX_FLUENT_SERVICE_SYMBOL_NAME = 'NgxFluentService';
5
+ /**
6
+ * ponytail: matches by resolved type-symbol name only, not by declaration file path.
7
+ * Upgrade to checking the symbol's declaring module specifier if a same-named type from
8
+ * another package ever produces a false positive in practice.
9
+ */
10
+ function isNgxFluentServiceCall(checker, expression) {
11
+ const type = checker.getTypeAtLocation(expression);
12
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
13
+ return symbol?.getName() === NGX_FLUENT_SERVICE_SYMBOL_NAME;
14
+ }
15
+ function isProjectSourceFile(sourceFile) {
16
+ return !sourceFile.isDeclarationFile && !sourceFile.fileName.includes('/node_modules/');
17
+ }
18
+ /** Loads a ts.Program from a tsconfig, following one level of project references to gather all root files. */
19
+ export function loadProgram(scanRoot, projectPath) {
20
+ const configPath = projectPath
21
+ ? resolve(scanRoot, projectPath)
22
+ : ts.findConfigFile(scanRoot, ts.sys.fileExists, 'tsconfig.json');
23
+ if (!configPath) {
24
+ throw new Error(`No tsconfig.json found starting from ${scanRoot}. Pass --project to point at one explicitly.`);
25
+ }
26
+ const fileNames = new Set();
27
+ const seen = new Set();
28
+ let rootOptions = {};
29
+ function loadConfig(path, isRoot) {
30
+ const resolvedPath = resolve(path);
31
+ if (seen.has(resolvedPath))
32
+ return;
33
+ seen.add(resolvedPath);
34
+ const readResult = ts.readConfigFile(resolvedPath, ts.sys.readFile);
35
+ if (readResult.error) {
36
+ throw new Error(ts.flattenDiagnosticMessageText(readResult.error.messageText, '\n'));
37
+ }
38
+ const parsed = ts.parseJsonConfigFileContent(readResult.config, ts.sys, dirname(resolvedPath));
39
+ for (const fileName of parsed.fileNames)
40
+ fileNames.add(fileName);
41
+ if (isRoot)
42
+ rootOptions = parsed.options;
43
+ for (const ref of parsed.projectReferences ?? []) {
44
+ loadConfig(ts.resolveProjectReferencePath(ref), false);
45
+ }
46
+ }
47
+ loadConfig(configPath, true);
48
+ if (fileNames.size === 0) {
49
+ throw new Error(`${configPath} resolved to zero source files (check its "include"/"references").`);
50
+ }
51
+ return ts.createProgram({ rootNames: [...fileNames], options: rootOptions });
52
+ }
53
+ /** Scans a ts.Program for NgxFluentService.translate() calls and @Component template sources. */
54
+ export function scanProgram(program) {
55
+ const checker = program.getTypeChecker();
56
+ const keyUsages = [];
57
+ const dynamicKeyWarnings = [];
58
+ const componentTemplates = [];
59
+ for (const sourceFile of program.getSourceFiles()) {
60
+ if (!isProjectSourceFile(sourceFile))
61
+ continue;
62
+ const lineOf = (node) => sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
63
+ const visit = (node) => {
64
+ if (ts.isCallExpression(node) &&
65
+ ts.isPropertyAccessExpression(node.expression) &&
66
+ node.expression.name.text === 'translate' &&
67
+ isNgxFluentServiceCall(checker, node.expression.expression)) {
68
+ const [keyArg] = node.arguments;
69
+ if (keyArg && ts.isStringLiteralLike(keyArg)) {
70
+ keyUsages.push({ key: keyArg.text, file: sourceFile.fileName, line: lineOf(node) });
71
+ }
72
+ else {
73
+ dynamicKeyWarnings.push({ file: sourceFile.fileName, line: lineOf(node) });
74
+ }
75
+ }
76
+ if (ts.isClassDeclaration(node)) {
77
+ for (const decorator of ts.getDecorators(node) ?? []) {
78
+ if (!ts.isCallExpression(decorator.expression) ||
79
+ !ts.isIdentifier(decorator.expression.expression) ||
80
+ decorator.expression.expression.text !== 'Component') {
81
+ continue;
82
+ }
83
+ const [arg] = decorator.expression.arguments;
84
+ if (!arg || !ts.isObjectLiteralExpression(arg))
85
+ continue;
86
+ for (const prop of arg.properties) {
87
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
88
+ continue;
89
+ if (prop.name.text === 'template' && ts.isStringLiteralLike(prop.initializer)) {
90
+ componentTemplates.push({ text: prop.initializer.text, file: sourceFile.fileName });
91
+ }
92
+ else if (prop.name.text === 'templateUrl' && ts.isStringLiteralLike(prop.initializer)) {
93
+ const templatePath = resolve(dirname(sourceFile.fileName), prop.initializer.text);
94
+ try {
95
+ componentTemplates.push({ text: readFileSync(templatePath, 'utf8'), file: templatePath });
96
+ }
97
+ catch {
98
+ // Referenced template file doesn't exist on disk — nothing to scan.
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ts.forEachChild(node, visit);
105
+ };
106
+ visit(sourceFile);
107
+ }
108
+ return { keyUsages, dynamicKeyWarnings, componentTemplates };
109
+ }
@@ -0,0 +1,22 @@
1
+ export interface ExtractConfig {
2
+ /** Locale code -> path to that locale's .ftl file, resolved relative to the config file's directory. */
3
+ locales: Record<string, string>;
4
+ }
5
+ export interface KeyUsage {
6
+ key: string;
7
+ file: string;
8
+ line: number;
9
+ }
10
+ export interface DynamicKeyWarning {
11
+ file: string;
12
+ line: number;
13
+ }
14
+ export interface LocaleReport {
15
+ missing: string[];
16
+ orphaned: string[];
17
+ parseError: string | null;
18
+ }
19
+ export interface Report {
20
+ locales: Record<string, LocaleReport>;
21
+ dynamicKeyWarnings: DynamicKeyWarning[];
22
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@apappas1129/ngx-fluent-extract",
3
+ "version": "0.1.0",
4
+ "description": "Extraction report CLI for ngx-fluent: diffs translation keys used in an Angular app's code against each locale's .ftl file.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "ngx-fluent-extract": "dist/cli.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc && node -e \"require('node:fs').chmodSync('dist/cli.js', 0o755)\"",
23
+ "test": "vitest run"
24
+ },
25
+ "dependencies": {
26
+ "@fluent/syntax": "^0.19.0"
27
+ },
28
+ "peerDependencies": {
29
+ "typescript": ">=5.0.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/apappas1129/ngx-fluent.git",
34
+ "directory": "ngx-fluent-extract"
35
+ },
36
+ "keywords": [
37
+ "i18n",
38
+ "l10n",
39
+ "localization",
40
+ "ftl",
41
+ "fluent",
42
+ "angular",
43
+ "cli"
44
+ ]
45
+ }