PAYATHON 2026

Angular Reactive Forms 如何切换语言并保留表单数据?

数据阿明

结论

建议按语言代码为每种语言建立独立的 FormGroup。切换语言时只切换当前显示的子表单,不删除或重新创建控件。这样,各语言已填写的内容都会保留;首次切换到还没有数据的语言时,字段会显示为空。

Language 和 Internal Key 可以放在外层表单中,标题、描述等语言相关字段则放在按语言代码分组的子表单里。下面的示例沿用问题中的 TranslationType 和 TranslationLanguage,并将 Title 显示为 Category Name,将 FullDescription 显示为 Category Description。

创建每种语言的子表单

下面的示例会在初始化时为每种语言创建一组控件,并从已有的 Translation[] 载入内容。为简化示例,代码使用 FormGroup<any>。如果项目启用了严格类型表单,可以为各组控件补充具体类型。

import { FormControl, FormGroup } from '@angular/forms';

enum TranslationType {
  Title = 0,
  ShortDescription = 1,
  FullDescription = 2,
  Meta = 3
}

enum TranslationLanguage {
  RO = 0,
  EN = 1,
  DE = 2,
  FR = 3
}

interface Translation {
  id: string;
  value: string;
  translationType: TranslationType;
  language: TranslationLanguage;
}

const languages = [
  TranslationLanguage.RO,
  TranslationLanguage.EN,
  TranslationLanguage.DE,
  TranslationLanguage.FR
];

const translationTypes = [
  TranslationType.Title,
  TranslationType.ShortDescription,
  TranslationType.FullDescription,
  TranslationType.Meta
];

export class CategoryEditorComponent {
  readonly languages = languages;
  readonly form: FormGroup<any>;
  selectedLanguage = TranslationLanguage.EN;

  constructor() {
    const existingTranslations: Translation[] = [];
    const languageGroups: Record<string, FormGroup<any>> = {};

    for (const language of this.languages) {
      const fields: Record<string, FormControl<string>> = {};

      for (const type of translationTypes) {
        const translation = existingTranslations.find(
          item => item.language === language && item.translationType === type
        );
        fields[String(type)] = new FormControl(translation?.value ?? '', {
          nonNullable: true
        });
      }

      languageGroups[String(language)] = new FormGroup(fields);
    }

    this.form = new FormGroup({
      internalKey: new FormControl('', { nonNullable: true }),
      translationsByLanguage: new FormGroup(languageGroups)
    });
  }

  get currentTranslationForm(): FormGroup<any> {
    return this.form.get([
      'translationsByLanguage',
      String(this.selectedLanguage)
    ]) as FormGroup<any>;
  }

  onLanguageChange(value: string | number): void {
    this.selectedLanguage = Number(value) as TranslationLanguage;
  }
}

实际组件中的 existingTranslations 应从正在编辑的分类中获取。没有对应翻译记录的字段会以空字符串作为初始值。将字段名设为 TranslationType 的枚举值后,提交时就可以直接映射回 Translation[]。

切换语言并显示子表单

语言选择器会更新当前语言,模板则绑定到对应的子表单:

<select
  [value]="selectedLanguage"
  (change)="onLanguageChange($any($event.target).value)"
>
  <option *ngFor="let language of languages" [value]="language">
    {{ language }}
  </option>
</select>

<form [formGroup]="form">
  <label>
    Internal Key
    <input formControlName="internalKey" />
  </label>

  <section [formGroup]="currentTranslationForm">
    <label>
      Category Name
      <input [formControlName]="TranslationType.Title" />
    </label>

    <label>
      Category Description
      <textarea [formControlName]="TranslationType.FullDescription"></textarea>
    </label>
  </section>
</form>

要在模板中使用 TranslationType.Title 和 TranslationType.FullDescription,需要将枚举暴露为组件属性:

readonly TranslationType = TranslationType;

切换语言时,变化的只是 currentTranslationForm 指向的子表单。各语言的控件仍保留在父表单中,所以用户填写 EN 后切换到 FR,再切回 EN,之前输入的内容还在。若切换时删除或重新创建控件,则需要另行保存并恢复它们的值。

提交时生成 Translation[]

保存时,可以遍历语言和字段,将有内容的字段转换为翻译记录。若需要保留现有记录的 id,可以按语言和类型查找并复用。新记录的 ID 则应按项目现有的生成或持久化方式处理。

function collectTranslations(
  translationsByLanguage: Record<string, Record<string, string>>,
  existingTranslations: Translation[]
): Translation[] {
  const result: Translation[] = [];

  for (const language of languages) {
    const values = translationsByLanguage[String(language)];

    for (const type of translationTypes) {
      const value = values[String(type)]?.trim() ?? '';
      if (!value) continue;

      const existing = existingTranslations.find(
        item => item.language === language && item.translationType === type
      );

      if (!existing) {
        // 新记录的 ID 按项目现有规则生成或由服务端创建。
        continue;
      }

      result.push({ ...existing, value });
    }
  }

  return result;
}

这段提交代码只更新已有翻译,并为新翻译预留了 ID 创建位置。实际保存逻辑还需补上项目使用的 ID 处理方式,并确认空字符串表示“删除翻译”还是“不保存该字段”。此外,问题中的 ObjectType 没有作为字段出现在 Translation 接口中;如果翻译记录还要关联产品或分类,保存方式应由实际数据模型和接口约定决定。

另一种表单组织方式

如果语言较多,或者用户需要同时编辑多种语言,也可以直接将所有语言以分栏或标签页的形式展示。数据模型仍然可以使用按语言分组的子表单,区别在于界面会同时显示多组字段。选择下拉框还是标签页,主要取决于交互设计。

备注:内容仅供参考。