author Yosuke Ota
  import type { AST as JSONAST } from 'jsonc-eslint-parser' ; import type { AST as YAMLAST } from 'yaml-eslint-parser' ; import { extname } from 'path' ; import { defineCustomBlocksVisitor , getLocaleMessages } from '../utils/index' ; import debugBuilder from 'debug' ; import type { I18nLocaleMessageDictionary , RuleContext , RuleListener } from '../types' ; import type { LocaleMessage , LocaleMessages } from '../utils/locale-messages' ; import { joinPath } from '../utils/key-path' ; const debug = debugBuilder ( 'eslint-plugin-vue-i18n:no-missing-keys-in-other-locales' ) ; function create ( context ) { const filename = context . getFilename ( ) ; const ignoreLocales = context . options [ 0 ] ?. ignoreLocales || [ ] ; function reportMissing ( keyPath , locales , reportNode ) { const quotedLocales = locales . map ( s => `'${ s }'` ) ; context . report ( { message : "'{{path}}' does not exist in {{locales}} locale(s)" , data : { path : joinPath ( ... keyPath ) , locales : [ quotedLocales . pop ( ) , quotedLocales . join ( ', ' ) ] . filter ( loc => loc ) . reverse ( ) . join ( ' and ' ) } , loc : reportNode . loc } ) ; } function isLeafMessageNode ( node ) { if ( node == null ) { return false ; } if ( node . type === 'JSONLiteral' ) { if ( node . value == null && node . regex == null && node . bigint == null ) { return false ; } return true ; } if ( node . type === 'JSONIdentifier' || node . type === 'JSONTemplateLiteral' ) { return true ; } if ( node . type === 'JSONUnaryExpression' ) { return isLeafMessageNode ( node . argument ) ; } if ( node . type === 'YAMLScalar' ) { if ( node . value == null ) { return false ; } return true ; } if ( node . type === 'YAMLWithMeta' ) { return isLeafMessageNode ( node . value ) ; } if ( node . type === 'YAMLAlias' ) { return true ; } return false ; } function createVerifyContext < N extends JSONAST . JSONNode | YAMLAST . YAMLNode > ( targetLocaleMessage , localeMessages ) { function getOtherLocaleMessages ( locale ) { const ignores = new Set ( [ ... ignoreLocales , locale ] ) ; return localeMessages . locales . filter ( locale => ! ignores . has ( locale ) ) . map ( locale => { return { locale , dictList : localeMessages . localeMessages . map ( lm => lm . getMessagesFromLocale ( locale ) ) } ; } ) ; } type KeyStack = { locale : null ; node ? : N ; upper ? : KeyStack ; otherLocaleMessages : null ; } | { locale : string ; node ? : N ; upper ? : KeyStack ; keyPath : ( string | number ) [ ] ; otherLocaleMessages : { locale : string ; dictList : I18nLocaleMessageDictionary [ ] ; } [ ] ; } ; let keyStack ; if ( targetLocaleMessage . isResolvedLocaleByFileName ( ) ) { const locale = targetLocaleMessage . locales [ 0 ] ; keyStack = { locale , otherLocaleMessages : getOtherLocaleMessages ( locale ) , keyPath : [ ] } ; } else { keyStack = { locale : null , otherLocaleMessages : null } ; } return { enterKey ( key , node , needsVerify ) { if ( keyStack . locale == null ) { const locale = key as string ; keyStack = { node , locale , otherLocaleMessages : getOtherLocaleMessages ( locale ) , keyPath : [ ] , upper : keyStack } ; } else { const keyPath = [ ... keyStack . keyPath , key ] ; const nextOtherLocaleMessages = [ ] ; const missingLocales = [ ] ; for ( const { locale , dictList } of keyStack . otherLocaleMessages ) { const nextDictList = [ ] ; let exists = false ; for ( const dict of dictList ) { const nextDict = dict [ key ] ; if ( nextDict != null ) { exists = true ; if ( typeof nextDict === 'object' ) { nextDictList . push ( nextDict ) ; } } } if ( ! exists && needsVerify ) { missingLocales . push ( locale ) ; } nextOtherLocaleMessages . push ( { locale , dictList : nextDictList } ) ; } if ( missingLocales . length ) { reportMissing ( keyPath , missingLocales , node ) ; } keyStack = { node , locale : keyStack . locale , otherLocaleMessages : nextOtherLocaleMessages , keyPath , upper : keyStack } ; } } , leaveKey ( node ) { if ( keyStack . node === node ) { keyStack = keyStack . upper ! ; } } } ; } /**
     * Create node visitor for JSON
     */ function createVisitorForJson ( targetLocaleMessage , localeMessages ) { const ctx = createVerifyContext ( targetLocaleMessage , localeMessages ) ; return { JSONProperty ( node ) { const key = node . key . type === 'JSONLiteral' ? `${ node . key . value }` : node . key . name ; ctx . enterKey ( key , node . key , isLeafMessageNode ( node . value ) ) ; } , 'JSONProperty:exit' ( node ) { ctx . leaveKey ( node . key ) ; } , 'JSONArrayExpression > *' ( node ) { const key = node . parent . elements . indexOf ( node ) ; ctx . enterKey ( key , node , isLeafMessageNode ( node ) ) ; } , 'JSONArrayExpression > *:exit' ( node ) { ctx . leaveKey ( node ) ; } } ; } /**
     * Create node visitor for YAML
     */ function createVisitorForYaml ( targetLocaleMessage , localeMessages ) { const yamlKeyNodes = new Set < YAMLAST . YAMLContent | YAMLAST . YAMLWithMeta > ( ) ; function withinKey ( node ) { for ( const keyNode of yamlKeyNodes ) { if ( keyNode . range [ 0 ] <= node . range [ 0 ] && node . range [ 0 ] < keyNode . range [ 1 ] ) { return true ; } } return false ; } const ctx = createVerifyContext ( targetLocaleMessage , localeMessages ) ; return { YAMLPair ( node ) { if ( node . key != null ) { if ( withinKey ( node ) ) { return ; } yamlKeyNodes . add ( node . key ) ; } if ( node . key != null && node . key . type === 'YAMLScalar' ) { const keyValue = node . key . value ; const key = typeof keyValue === 'string' ? keyValue : String ( keyValue ) ; ctx . enterKey ( key , node . key , isLeafMessageNode ( node . value ) ) ; } } , 'YAMLPair:exit' ( node ) { if ( node . key != null ) { ctx . leaveKey ( node . key ) ; } } , 'YAMLSequence > *' ( node ) { if ( withinKey ( node ) ) { return ; } const key = node . parent . entries . indexOf ( node ) ; ctx . enterKey ( key , node , isLeafMessageNode ( node ) ) ; } , 'YAMLSequence > *:exit' ( node ) { ctx . leaveKey ( node ) ; } } ; } if ( extname ( filename ) === '.vue' ) { return defineCustomBlocksVisitor ( context , ctx => { const localeMessages = getLocaleMessages ( context ) ; const targetLocaleMessage = localeMessages . findBlockLocaleMessage ( ctx . parserServices . customBlock ) ; if ( ! targetLocaleMessage ) { return { } ; } return createVisitorForJson ( targetLocaleMessage , localeMessages ) ; } , ctx => { const localeMessages = getLocaleMessages ( context ) ; const targetLocaleMessage = localeMessages . findBlockLocaleMessage ( ctx . parserServices . customBlock ) ; if ( ! targetLocaleMessage ) { return { } ; } return createVisitorForYaml ( targetLocaleMessage , localeMessages ) ; } ) ; } else if ( context . parserServices . isJSON || context . parserServices . isYAML ) { const localeMessages = getLocaleMessages ( context ) ; const targetLocaleMessage = localeMessages . findExistLocaleMessage ( filename ) ; if ( ! targetLocaleMessage ) { debug ( `ignore ${ filename } in no-missing-keys-in-other-locales` ) ; return { } ; } if ( context . parserServices . isJSON ) { return createVisitorForJson ( targetLocaleMessage , localeMessages ) ; } else if ( context . parserServices . isYAML ) { return createVisitorForYaml ( targetLocaleMessage , localeMessages ) ; } return { } ; } else { debug ( `ignore ${ filename } in no-missing-keys-in-other-locales` ) ; return { } ; } } export = { meta : { type : 'layout' , docs : { description : 'disallow missing locale message keys in other locales' , category : 'Best Practices' , recommended : false } , fixable : null , schema : [ { type : 'object' , properties : { ignoreLocales : { type : 'array' , items : { type : 'string' } } } , additionalProperties : false } ] } , create } ;