diff --git a/packages/angular_devkit/build_angular/src/babel/presets/application.ts b/packages/angular_devkit/build_angular/src/babel/presets/application.ts
index 8b849f07d6c4..c5ed477949f2 100644
--- a/packages/angular_devkit/build_angular/src/babel/presets/application.ts
+++ b/packages/angular_devkit/build_angular/src/babel/presets/application.ts
@@ -36,6 +36,7 @@ export interface ApplicationPresetOptions {
     locale: string;
     missingTranslationBehavior?: 'error' | 'warning' | 'ignore';
     translation?: unknown;
+    translationFiles?: string[];
     pluginCreators?: I18nPluginCreators;
   };
 
diff --git a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts
index d74a9e753643..2e44db09c02d 100644
--- a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts
+++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts
@@ -62,6 +62,7 @@ async function requiresLinking(path: string, source: string): Promise<boolean> {
   return needsLinking(path, source);
 }
 
+// eslint-disable-next-line max-lines-per-function
 export default custom<ApplicationPresetOptions>(() => {
   const baseOptions = Object.freeze({
     babelrc: false,
@@ -149,6 +150,18 @@ export default custom<ApplicationPresetOptions>(() => {
           ...(i18n as NonNullable<ApplicationPresetOptions['i18n']>),
           pluginCreators: i18nPluginCreators,
         };
+
+        // Add translation files as dependencies of the file to support rebuilds
+        // Except for `@angular/core` which needs locale injection but has no translations
+        if (
+          customOptions.i18n.translationFiles &&
+          !/[\\/]@angular[\\/]core/.test(this.resourcePath)
+        ) {
+          for (const file of customOptions.i18n.translationFiles) {
+            this.addDependency(file);
+          }
+        }
+
         shouldProcess = true;
       }
 
diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/index.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/index.ts
index af470f77a406..208c095bac90 100644
--- a/packages/angular_devkit/build_angular/src/builders/dev-server/index.ts
+++ b/packages/angular_devkit/build_angular/src/builders/dev-server/index.ts
@@ -23,8 +23,9 @@ import { ExecutionTransformer } from '../../transforms';
 import { normalizeOptimization } from '../../utils';
 import { checkPort } from '../../utils/check-port';
 import { colors } from '../../utils/color';
-import { I18nOptions } from '../../utils/i18n-options';
+import { I18nOptions, loadTranslations } from '../../utils/i18n-options';
 import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
+import { createTranslationLoader } from '../../utils/load-translations';
 import { NormalizedCachedOptions, normalizeCacheOptions } from '../../utils/normalize-cache';
 import { generateEntryPoints } from '../../utils/package-chunk-sort';
 import { assertCompatibleAngularVersion } from '../../utils/version';
@@ -33,6 +34,7 @@ import {
   getIndexInputFile,
   getIndexOutputFile,
 } from '../../utils/webpack-browser-config';
+import { addError, addWarning } from '../../utils/webpack-diagnostics';
 import {
   getAnalyticsConfig,
   getCommonConfig,
@@ -192,7 +194,7 @@ export function serveWebpackBrowser(
         );
       }
 
-      await setupLocalize(locale, i18n, browserOptions, webpackConfig, cacheOptions);
+      await setupLocalize(locale, i18n, browserOptions, webpackConfig, cacheOptions, context);
     }
 
     if (transforms.webpackConfiguration) {
@@ -288,6 +290,7 @@ async function setupLocalize(
   browserOptions: BrowserBuilderSchema,
   webpackConfig: webpack.Configuration,
   cacheOptions: NormalizedCachedOptions,
+  context: BuilderContext,
 ) {
   const localeDescription = i18n.locales[locale];
 
@@ -320,6 +323,9 @@ async function setupLocalize(
     locale,
     missingTranslationBehavior,
     translation: i18n.shouldInline ? translation : undefined,
+    translationFiles: localeDescription?.files.map((file) =>
+      path.resolve(context.workspaceRoot, file.path),
+    ),
   };
 
   const i18nRule: webpack.RuleSetRule = {
@@ -351,6 +357,33 @@ async function setupLocalize(
   }
 
   rules.push(i18nRule);
+
+  // Add a plugin to reload translation files on rebuilds
+  const loader = await createTranslationLoader();
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  webpackConfig.plugins!.push({
+    apply: (compiler: webpack.Compiler) => {
+      compiler.hooks.thisCompilation.tap('build-angular', (compilation) => {
+        if (i18n.shouldInline && i18nLoaderOptions.translation === undefined) {
+          // Reload translations
+          loadTranslations(locale, localeDescription, context.workspaceRoot, loader, {
+            warn(message) {
+              addWarning(compilation, message);
+            },
+            error(message) {
+              addError(compilation, message);
+            },
+          });
+          i18nLoaderOptions.translation = localeDescription.translation;
+        }
+
+        compilation.hooks.finishModules.tap('build-angular', () => {
+          // After loaders are finished, clear out the now unneeded translations
+          i18nLoaderOptions.translation = undefined;
+        });
+      });
+    },
+  });
 }
 
 export default createBuilder<DevServerBuilderOptions, DevServerBuilderOutput>(serveWebpackBrowser);
diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts
new file mode 100644
index 000000000000..aea5f2ff5889
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts
@@ -0,0 +1,109 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/* eslint-disable max-len */
+import fetch from 'node-fetch'; // eslint-disable-line import/no-extraneous-dependencies
+import { concatMap, count, take, timeout } from 'rxjs/operators';
+import { URL } from 'url';
+import { serveWebpackBrowser } from '../../index';
+import {
+  BASE_OPTIONS,
+  BUILD_TIMEOUT,
+  DEV_SERVER_BUILDER_INFO,
+  describeBuilder,
+  setupBrowserTarget,
+} from '../setup';
+
+describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => {
+  describe('Behavior: "i18n translation file watching"', () => {
+    beforeEach(() => {
+      harness.useProject('test', {
+        root: '.',
+        sourceRoot: 'src',
+        cli: {
+          cache: {
+            enabled: false,
+          },
+        },
+        i18n: {
+          locales: {
+            'fr': 'src/locales/messages.fr.xlf',
+          },
+        },
+      });
+
+      setupBrowserTarget(harness, { localize: ['fr'] });
+    });
+
+    it('watches i18n translation files by default', async () => {
+      harness.useTarget('serve', {
+        ...BASE_OPTIONS,
+      });
+
+      await harness.writeFile(
+        'src/app/app.component.html',
+        `
+          <p id="hello" i18n="An introduction header for this sample">Hello {{ title }}! </p>
+        `,
+      );
+
+      await harness.writeFile('src/locales/messages.fr.xlf', TRANSLATION_FILE_CONTENT);
+
+      const buildCount = await harness
+        .execute()
+        .pipe(
+          timeout(BUILD_TIMEOUT * 2),
+          concatMap(async ({ result }, index) => {
+            expect(result?.success).toBe(true);
+
+            const mainUrl = new URL('main.js', `${result?.baseUrl}`);
+
+            switch (index) {
+              case 0: {
+                const response = await fetch(mainUrl);
+                expect(await response?.text()).toContain('Bonjour');
+
+                await harness.modifyFile('src/locales/messages.fr.xlf', (content) =>
+                  content.replace('Bonjour', 'Salut'),
+                );
+                break;
+              }
+              case 1: {
+                const response = await fetch(mainUrl);
+                expect(await response?.text()).toContain('Salut');
+                break;
+              }
+            }
+          }),
+          take(2),
+          count(),
+        )
+        .toPromise();
+
+      expect(buildCount).toBe(2);
+    });
+  });
+});
+
+const TRANSLATION_FILE_CONTENT = `
+  <?xml version="1.0" encoding="UTF-8" ?>
+  <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+    <file target-language="en-US" datatype="plaintext" original="ng2.template">
+      <body>
+        <trans-unit id="4286451273117902052" datatype="html">
+          <target>Bonjour <x id="INTERPOLATION" equiv-text="{{ title }}"/>! </target>
+          <context-group purpose="location">
+            <context context-type="targetfile">src/app/app.component.html</context>
+            <context context-type="linenumber">2,3</context>
+          </context-group>
+          <note priority="1" from="description">An introduction header for this sample</note>
+        </trans-unit>
+      </body>
+    </file>
+  </xliff>
+`;
diff --git a/packages/angular_devkit/build_angular/src/utils/i18n-options.ts b/packages/angular_devkit/build_angular/src/utils/i18n-options.ts
index 0106b7064260..9d57aceb44b8 100644
--- a/packages/angular_devkit/build_angular/src/utils/i18n-options.ts
+++ b/packages/angular_devkit/build_angular/src/utils/i18n-options.ts
@@ -15,25 +15,28 @@ import path from 'path';
 import { Schema as BrowserBuilderSchema } from '../builders/browser/schema';
 import { Schema as ServerBuilderSchema } from '../builders/server/schema';
 import { readTsconfig } from '../utils/read-tsconfig';
-import { createTranslationLoader } from './load-translations';
+import { TranslationLoader, createTranslationLoader } from './load-translations';
 
 /**
  * The base module location used to search for locale specific data.
  */
 const LOCALE_DATA_BASE_MODULE = '@angular/common/locales/global';
 
+export interface LocaleDescription {
+  files: {
+    path: string;
+    integrity?: string;
+    format?: string;
+  }[];
+  translation?: Record<string, unknown>;
+  dataPath?: string;
+  baseHref?: string;
+}
+
 export interface I18nOptions {
   inlineLocales: Set<string>;
   sourceLocale: string;
-  locales: Record<
-    string,
-    {
-      files: { path: string; integrity?: string; format?: string }[];
-      translation?: Record<string, unknown>;
-      dataPath?: string;
-      baseHref?: string;
-    }
-  >;
+  locales: Record<string, LocaleDescription>;
   flatOutput?: boolean;
   readonly shouldInline: boolean;
   hasDefinedSourceLocale?: boolean;
@@ -218,48 +221,27 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
       loader = await createTranslationLoader();
     }
 
-    for (const file of desc.files) {
-      const loadResult = loader(path.join(context.workspaceRoot, file.path));
-
-      for (const diagnostics of loadResult.diagnostics.messages) {
-        if (diagnostics.type === 'error') {
-          throw new Error(`Error parsing translation file '${file.path}': ${diagnostics.message}`);
-        } else {
-          context.logger.warn(`WARNING [${file.path}]: ${diagnostics.message}`);
-        }
-      }
-
-      if (loadResult.locale !== undefined && loadResult.locale !== locale) {
-        context.logger.warn(
-          `WARNING [${file.path}]: File target locale ('${loadResult.locale}') does not match configured locale ('${locale}')`,
-        );
-      }
-
-      usedFormats.add(loadResult.format);
-      if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) {
-        // This limitation is only for legacy message id support (defaults to true as of 9.0)
-        throw new Error(
-          'Localization currently only supports using one type of translation file format for the entire application.',
-        );
-      }
-
-      file.format = loadResult.format;
-      file.integrity = loadResult.integrity;
-
-      if (desc.translation) {
-        // Merge translations
-        for (const [id, message] of Object.entries(loadResult.translations)) {
-          if (desc.translation[id] !== undefined) {
-            context.logger.warn(
-              `WARNING [${file.path}]: Duplicate translations for message '${id}' when merging`,
-            );
-          }
-          desc.translation[id] = message;
-        }
-      } else {
-        // First or only translation file
-        desc.translation = loadResult.translations;
-      }
+    loadTranslations(
+      locale,
+      desc,
+      context.workspaceRoot,
+      loader,
+      {
+        warn(message) {
+          context.logger.warn(message);
+        },
+        error(message) {
+          throw new Error(message);
+        },
+      },
+      usedFormats,
+    );
+
+    if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) {
+      // This limitation is only for legacy message id support (defaults to true as of 9.0)
+      throw new Error(
+        'Localization currently only supports using one type of translation file format for the entire application.',
+      );
     }
   }
 
@@ -294,3 +276,49 @@ function findLocaleDataPath(locale: string, resolver: (locale: string) => string
     return null;
   }
 }
+
+export function loadTranslations(
+  locale: string,
+  desc: LocaleDescription,
+  workspaceRoot: string,
+  loader: TranslationLoader,
+  logger: { warn: (message: string) => void; error: (message: string) => void },
+  usedFormats?: Set<string>,
+) {
+  for (const file of desc.files) {
+    const loadResult = loader(path.join(workspaceRoot, file.path));
+
+    for (const diagnostics of loadResult.diagnostics.messages) {
+      if (diagnostics.type === 'error') {
+        logger.error(`Error parsing translation file '${file.path}': ${diagnostics.message}`);
+      } else {
+        logger.warn(`WARNING [${file.path}]: ${diagnostics.message}`);
+      }
+    }
+
+    if (loadResult.locale !== undefined && loadResult.locale !== locale) {
+      logger.warn(
+        `WARNING [${file.path}]: File target locale ('${loadResult.locale}') does not match configured locale ('${locale}')`,
+      );
+    }
+
+    usedFormats?.add(loadResult.format);
+    file.format = loadResult.format;
+    file.integrity = loadResult.integrity;
+
+    if (desc.translation) {
+      // Merge translations
+      for (const [id, message] of Object.entries(loadResult.translations)) {
+        if (desc.translation[id] !== undefined) {
+          logger.warn(
+            `WARNING [${file.path}]: Duplicate translations for message '${id}' when merging`,
+          );
+        }
+        desc.translation[id] = message;
+      }
+    } else {
+      // First or only translation file
+      desc.translation = loadResult.translations;
+    }
+  }
+}