angular/angular · warning

Required "translations" property missing.

Error message

Required "translations" property missing.

What it means

The parsed JSON object is missing the `translations` property. Because the earlier precheck only looked for the substring `"translations"` in the raw text, the word can appear (nested or in a value) while the required top-level key is absent, which is exactly what triggers this warning and a canParse=false verdict.

Source

Thrown at packages/localize/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts:59

    if (
      extname(filePath) !== '.json' ||
      !(contents.includes('"locale"') && contents.includes('"translations"'))
    ) {
      diagnostics.warn('File does not have .json extension.');
      return {canParse: false, diagnostics};
    }
    try {
      const json = JSON.parse(contents) as SimpleJsonFile;
      if (json.locale === undefined) {
        diagnostics.warn('Required "locale" property missing.');
        return {canParse: false, diagnostics};
      }
      if (typeof json.locale !== 'string') {
        diagnostics.warn('The "locale" property is not a string.');
        return {canParse: false, diagnostics};
      }
      if (json.translations === undefined) {
        diagnostics.warn('Required "translations" property missing.');
        return {canParse: false, diagnostics};
      }
      if (typeof json.translations !== 'object') {
        diagnostics.warn('The "translations" is not an object.');
        return {canParse: false, diagnostics};
      }
      return {canParse: true, diagnostics, hint: json};
    } catch (e) {
      diagnostics.warn('File is not valid JSON.');
      return {canParse: false, diagnostics};
    }
  }

  parse(_filePath: string, contents: string, json?: SimpleJsonFile): ParsedTranslationBundle {
    const {locale: parsedLocale, translations} = json || (JSON.parse(contents) as SimpleJsonFile);
    const parsedTranslations: Record<MessageId, ɵParsedTranslation> = {};
    for (const messageId in translations) {
      const targetMessage = translations[messageId];

View on GitHub (pinned to 51cb07e980)

Solutions

  1. Rename the message map key to `translations` so it sits at the top level.
  2. If the file intentionally uses another key, transform it when generating the file rather than renaming the extension.
  3. Validate the schema (`locale` string + `translations` object at top level) before invoking the translate step.

Example fix

// before
{ "locale": "fr", "messages": { "hello": "Bonjour" } }

// after
{ "locale": "fr", "translations": { "hello": "Bonjour" } }
Defensive patterns

Strategy: type-guard

Validate before calling

const json: unknown = JSON.parse(readFileSync(path, 'utf8'));
if (!isSimpleJsonFile(json)) throw new Error(`${path}: top-level 'translations' object is required`);

Type guard

function hasTranslations(v: unknown): v is {translations: Record<string, unknown>} {
  return typeof v === 'object' && v !== null && 'translations' in v
    && typeof (v as {translations: unknown}).translations === 'object';
}

Prevention

When it happens

Trigger: A `.json` file with a top-level `locale` but translations nested under another key, e.g. `{ "locale": "fr", "messages": { ... } }` - the substring `"translations"` may occur in a comment-ish value while the key itself is absent.

Common situations: Generators that name the map `strings`, `messages`, or `dict` instead of `translations`; partial hand-written files.

Related errors


AI-assisted analysis of angular/angular@51cb07e980 (2026-08-22). Data as JSON: /api/errors/7f4967d892c04a1b. Report an issue: GitHub.