Skip to content

Commit b3654b6

Browse files
committed
feat(i18n): honor localize: [...] per-locale subset
Switch `--localize` from a bool flag to an optional comma-separated list so CI builds can emit just the locales they need. ngc-rs build --localize # all i18n.locales (unchanged) ngc-rs build --localize=en-US,de # only those two subdirs The architect builder now serializes `localize: ['en', 'de']` as `--localize=en,de` instead of dropping the array and warning. An empty array still falls back to "all locales" to match `@angular/build`. `fan_out_locales` validates each subset entry against the source locale and `i18n.locales` keys; an unknown locale fails the build with a clear error rather than silently producing an empty `dist/`.
1 parent a09ab0e commit b3654b6

8 files changed

Lines changed: 103 additions & 41 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ resolver = "2"
33
members = ["crates/cli", "crates/diagnostics", "crates/project-resolver", "crates/ts-transform", "crates/bundler", "crates/template-compiler", "crates/npm-resolver", "crates/linker", "crates/watch", "crates/dev-server"]
44

55
[workspace.package]
6-
version = "0.10.10"
6+
version = "0.10.11"
77
edition = "2021"
88
license = "MIT OR Apache-2.0"
99
authors = ["lukekania"]

crates/cli/src/main.rs

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,12 @@ enum Commands {
177177
/// every `$localize\`...\`` literal in the bundled output. The
178178
/// source-locale build is moved under
179179
/// `<out_dir>/<sourceLocale>/`.
180-
#[arg(long)]
181-
localize: bool,
180+
///
181+
/// Pass `--localize` alone to emit every locale declared in
182+
/// `i18n.locales`; pass `--localize=en,de` to restrict the output
183+
/// to a subset (useful for trimming CI builds).
184+
#[arg(long, num_args = 0..=1, value_delimiter = ',')]
185+
localize: Option<Vec<String>>,
182186
/// Treat any template that would fall back to JIT compilation as a
183187
/// hard error. Mirrors `@angular/build:application`, which has no
184188
/// JIT fallback. Defaults to on for `--configuration production` and
@@ -203,8 +207,11 @@ enum Commands {
203207
configuration: Option<String>,
204208
/// Emit one `<out_dir>/<locale>/` tree per locale defined in
205209
/// `angular.json`'s `i18n.locales` block.
206-
#[arg(long)]
207-
localize: bool,
210+
///
211+
/// Pass `--localize` alone for all locales, or `--localize=en,de`
212+
/// to restrict the output to a subset.
213+
#[arg(long, num_args = 0..=1, value_delimiter = ',')]
214+
localize: Option<Vec<String>>,
208215
},
209216
/// Serve the project: build once, watch for changes, and host the
210217
/// resulting `dist/` directory over HTTP with live reload. Mirrors
@@ -320,7 +327,7 @@ fn main() {
320327
&project,
321328
out_dir.as_deref(),
322329
configuration.as_deref(),
323-
localize,
330+
localize.as_deref(),
324331
Vec::new(),
325332
|_| false,
326333
) {
@@ -387,7 +394,7 @@ fn main() {
387394
&project,
388395
out_dir.as_deref(),
389396
configuration.as_deref(),
390-
localize,
397+
localize.as_deref(),
391398
strict_templates,
392399
) {
393400
Ok(result) => {
@@ -475,11 +482,16 @@ fn init_tracing() {
475482
}
476483

477484
/// Orchestrate the full build pipeline: resolve → transform → bundle → output.
485+
///
486+
/// `localize` mirrors the `--localize` CLI flag: `None` skips locale
487+
/// fan-out entirely; `Some(&[])` emits every locale declared in
488+
/// `i18n.locales`; `Some(&["en", "de"])` restricts the output to that
489+
/// subset.
478490
fn run_build(
479491
project: &Path,
480492
out_dir_override: Option<&Path>,
481493
configuration: Option<&str>,
482-
localize: bool,
494+
localize: Option<&[String]>,
483495
strict_templates: bool,
484496
) -> NgcResult<BuildResult> {
485497
run_build_with_options(
@@ -505,7 +517,7 @@ pub(crate) fn run_build_with_cache(
505517
project: &Path,
506518
out_dir_override: Option<&Path>,
507519
configuration: Option<&str>,
508-
localize: bool,
520+
localize: Option<&[String]>,
509521
cache: Option<&mut incremental::BuildCache>,
510522
) -> NgcResult<BuildResult> {
511523
run_build_with_options(
@@ -533,7 +545,7 @@ pub(crate) fn run_build_with_options(
533545
project: &Path,
534546
out_dir_override: Option<&Path>,
535547
configuration: Option<&str>,
536-
localize: bool,
548+
localize: Option<&[String]>,
537549
strict_templates: bool,
538550
mut cache: Option<&mut incremental::BuildCache>,
539551
base_href_override: Option<&str>,
@@ -1224,7 +1236,7 @@ pub(crate) fn run_build_with_options(
12241236
// every other writer so it sees the final filenames + contents.
12251237
if let Some(ref ap) = angular_project {
12261238
if ap.service_worker {
1227-
if localize {
1239+
if localize.is_some() {
12281240
tracing::warn!(
12291241
"serviceWorker is enabled but --localize was passed; skipping ngsw.json (per-locale manifests are not yet supported)"
12301242
);
@@ -1237,8 +1249,10 @@ pub(crate) fn run_build_with_options(
12371249

12381250
// Step 13: --localize → fan the source-locale build out to
12391251
// `<out_dir>/<sourceLocale>/` and produce a translated copy under
1240-
// `<out_dir>/<locale>/` for each entry in `i18n.locales`.
1241-
if localize {
1252+
// `<out_dir>/<locale>/` for each entry in `i18n.locales`. A non-empty
1253+
// `subset` filters the emitted locales — useful for trimming CI builds
1254+
// that only need one or two locales per deploy.
1255+
if let Some(subset) = localize {
12421256
let i18n = angular_project
12431257
.as_ref()
12441258
.and_then(|ap| ap.i18n.as_ref())
@@ -1247,7 +1261,7 @@ pub(crate) fn run_build_with_options(
12471261
"--localize was passed but angular.json does not declare a `projects.<name>.i18n` block"
12481262
.to_string(),
12491263
})?;
1250-
let localized_files = fan_out_locales(&out_dir, i18n, &output_files)?;
1264+
let localized_files = fan_out_locales(&out_dir, i18n, subset, &output_files)?;
12511265
output_files = localized_files;
12521266
}
12531267

@@ -1351,11 +1365,42 @@ pub(crate) fn run_build_with_options(
13511365
/// Move the source-locale build under `<out_dir>/<sourceLocale>/` and
13521366
/// emit a translated copy under `<out_dir>/<locale>/` for every entry in
13531367
/// `i18n.locales`. Returns the new full set of output files.
1368+
///
1369+
/// `subset` filters which locales are emitted. An empty slice emits every
1370+
/// locale (source plus all `i18n.locales` entries); a non-empty slice
1371+
/// restricts the output to the codes listed (validated against
1372+
/// `i18n.source_locale` and the keys of `i18n.locales`).
13541373
fn fan_out_locales(
13551374
out_dir: &Path,
13561375
i18n: &I18nConfig,
1376+
subset: &[String],
13571377
original_files: &[PathBuf],
13581378
) -> NgcResult<Vec<PathBuf>> {
1379+
let include_source: bool;
1380+
let include_locale: Box<dyn Fn(&str) -> bool>;
1381+
if subset.is_empty() {
1382+
include_source = true;
1383+
include_locale = Box::new(|_: &str| true);
1384+
} else {
1385+
// Reject `--localize=foo` when `foo` is neither the source locale
1386+
// nor one of the declared `i18n.locales` keys — silently skipping
1387+
// would let typos produce empty `dist/` runs in CI.
1388+
for code in subset {
1389+
let known = code == &i18n.source_locale || i18n.locales.contains_key(code.as_str());
1390+
if !known {
1391+
return Err(NgcError::ConfigError {
1392+
message: format!(
1393+
"--localize subset entry `{code}` is not declared in angular.json `i18n.locales` (and is not the source locale `{}`)",
1394+
i18n.source_locale
1395+
),
1396+
});
1397+
}
1398+
}
1399+
include_source = subset.iter().any(|c| c == &i18n.source_locale);
1400+
let allow: std::collections::BTreeSet<String> = subset.iter().cloned().collect();
1401+
include_locale = Box::new(move |code: &str| allow.contains(code));
1402+
}
1403+
13591404
// Materialize file contents from the original (source-locale) build so
13601405
// we can write them back into per-locale directories without worrying
13611406
// about the source-locale move clobbering them.
@@ -1377,10 +1422,15 @@ fn fan_out_locales(
13771422

13781423
let mut new_outputs: Vec<PathBuf> = Vec::new();
13791424

1380-
let source_dir = out_dir.join(&i18n.source_locale);
1381-
write_locale_tree(&source_dir, &sources, None, &mut new_outputs)?;
1425+
if include_source {
1426+
let source_dir = out_dir.join(&i18n.source_locale);
1427+
write_locale_tree(&source_dir, &sources, None, &mut new_outputs)?;
1428+
}
13821429

13831430
for entry in i18n.locales.values() {
1431+
if !include_locale(entry.locale.as_str()) {
1432+
continue;
1433+
}
13841434
let translations = match &entry.translation_path {
13851435
Some(path) => Some(localize::parse_xliff(path)?),
13861436
None => None,

crates/cli/src/serve_cmd.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub(crate) fn run_with_stop(
7474
project,
7575
None,
7676
configuration,
77-
false,
77+
None,
7878
false,
7979
Some(&mut cache),
8080
normalized_serve_path.as_deref(),
@@ -126,7 +126,7 @@ pub(crate) fn run_with_stop(
126126
&project_path,
127127
None,
128128
configuration_owned.as_deref(),
129-
false,
129+
None,
130130
false,
131131
Some(&mut cache),
132132
serve_path_owned.as_deref(),

crates/cli/src/watch_cmd.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ pub fn run(
2525
project: &Path,
2626
out_dir_override: Option<&Path>,
2727
configuration: Option<&str>,
28-
localize: bool,
28+
localize: Option<&[String]>,
2929
subscribers: Vec<Arc<dyn ngc_watch::WatchSubscriber>>,
3030
should_stop: impl FnMut(usize) -> bool,
3131
) -> NgcResult<()> {
3232
let mut cache = BuildCache::new();
33+
let localize_owned: Option<Vec<String>> = localize.map(|s| s.to_vec());
3334

3435
// Initial build to populate the cache. `run_build_with_cache` always
3536
// disables `strict_templates` — `watch` is a dev workflow, so JIT
@@ -39,7 +40,7 @@ pub fn run(
3940
project,
4041
out_dir_override,
4142
configuration,
42-
localize,
43+
localize_owned.as_deref(),
4344
Some(&mut cache),
4445
)?;
4546
eprintln!(
@@ -76,7 +77,7 @@ pub fn run(
7677
&project_path,
7778
out_dir_path.as_deref(),
7879
configuration.as_deref(),
79-
localize,
80+
localize_owned.as_deref(),
8081
Some(&mut cache),
8182
)?;
8283
eprintln!(

packages/builder/schemas/application.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@
275275
{ "type": "boolean" },
276276
{ "type": "array", "items": { "type": "string" } }
277277
],
278-
"description": "Generate per-locale builds. When set to true ngc-rs is invoked with `--localize` and emits one output tree per `i18n.locales` entry in angular.json. Selecting a locale subset (array form) is NOT yet honoured by ngc-rs and logs a warning."
278+
"description": "Generate per-locale builds. When set to true ngc-rs is invoked with `--localize` and emits one output tree per `i18n.locales` entry in angular.json. The array form `[\"en\", \"de\"]` serializes as `--localize=en,de` and restricts the output to the listed locales (useful for trimming CI builds)."
279279
},
280280
"inlineStyleLanguage": {
281281
"type": "string",

packages/builder/src/build/__tests__/options.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,25 @@ describe('translateOptions (build)', () => {
7676
expect(unset.args).not.toContain('--strict-templates');
7777
});
7878

79-
it('appends --localize and warns when localize is an array (subset not yet honoured)', () => {
79+
it('serializes a localize array as --localize=en,de (subset)', () => {
8080
const t = translateOptions(
8181
{ ...minimal, localize: ['en', 'de'] },
8282
'/ws',
8383
null,
8484
);
85+
expect(t.args).toContain('--localize=en,de');
86+
expect(t.args).not.toContain('--localize');
87+
expect(t.warnings.some((w) => w.includes('locale subset'))).toBe(false);
88+
});
89+
90+
it('treats an empty localize array as `--localize` (all locales)', () => {
91+
const t = translateOptions(
92+
{ ...minimal, localize: [] },
93+
'/ws',
94+
null,
95+
);
8596
expect(t.args).toContain('--localize');
86-
expect(t.warnings.some((w) => w.includes('locale subset'))).toBe(true);
97+
expect(t.args.some((a) => a.startsWith('--localize='))).toBe(false);
8798
});
8899

89100
it('accepts non-empty scripts arrays without error', () => {

packages/builder/src/build/options.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,6 @@ export function translateOptions(
175175
'The `outputHashing` option is hardcoded by ngc-rs per `--configuration` (production hashes bundles, development does not); the option value is ignored.',
176176
);
177177
}
178-
if (Array.isArray(raw.localize)) {
179-
warnings.push(
180-
'Selecting a locale subset via `localize` array is not yet honoured by ngc-rs; all locales declared in `angular.json` `i18n.locales` are emitted.',
181-
);
182-
}
183178
if (raw.stylePreprocessorOptions) {
184179
const opts = raw.stylePreprocessorOptions as json.JsonObject;
185180
const includePaths = opts['includePaths'];
@@ -223,7 +218,6 @@ export function translateOptions(
223218

224219
const tsConfig = raw.tsConfig ?? 'tsconfig.json';
225220
const outDir = resolveOutDir(raw.outputPath, workspaceRoot);
226-
const localize = raw.localize === true || Array.isArray(raw.localize);
227221

228222
const args: string[] = ['build', '--project', tsConfig, '--output-json'];
229223
if (configuration) {
@@ -232,8 +226,14 @@ export function translateOptions(
232226
if (outDir) {
233227
args.push('--out-dir', outDir);
234228
}
235-
if (localize) {
229+
// `localize: true` → emit all locales (`--localize` with no value).
230+
// `localize: ['en', 'de']` → emit just that subset (`--localize=en,de`).
231+
// `localize: []` is treated as `true` to match `@angular/build`, which
232+
// ignores an empty array and falls back to "all locales".
233+
if (raw.localize === true || (Array.isArray(raw.localize) && raw.localize.length === 0)) {
236234
args.push('--localize');
235+
} else if (Array.isArray(raw.localize)) {
236+
args.push(`--localize=${raw.localize.join(',')}`);
237237
}
238238
if (raw.strictTemplates === true) {
239239
args.push('--strict-templates');

0 commit comments

Comments
 (0)