import { EnumTypeDefinitionNode } from 'graphql' ; import { Entity , StringFile , Locale , TranslationStringObject } from './types' ; import typeDefs from './type_defs/schema.graphql' ; import { Octokit } from '@octokit/core' ; import fetch from 'node-fetch' ; import localesYAML from './data/locales.yml' ; import yaml from 'js-yaml' ; import marked from 'marked' ; import { logToFile } from './debug' ; import { readdir , readFile } from 'fs/promises' ; import last from 'lodash/last' ; import { loadOrGetEntities } from './entities' ; let locales = [ ] ; const octokit = new Octokit ( { auth : process . env . GITHUB_TOKEN } ) ; export const loadOrGetLocales = async ( ) => { if ( locales . length === 0 ) { locales = await loadLocales ( ) ; } return locales ; } ; export const loadFromGitHub = async ( localesWithRepos ) => { let locales = [ ] ; let i = 0 ; for ( const locale of localesWithRepos ) { i ++ ; console . log ( `-> loading repo ${ locale . repo } (${ i }/${ localesWithRepos . length })` ) ; locale . stringFiles = [ ] ; const [ owner , repo ] = locale . repo . split ( '/' ) ; const options = { owner , repo , path : '' } ; const contents = await octokit . request ( 'GET /repos/{owner}/{repo}/contents/{path}' , options ) ; const files = contents . data as any [ ] ; for ( const file of files ) { const extension = last ( file ?. name . split ( '.' ) ) || '' ; if ( [ 'yml' , 'yaml' ] . includes ( extension ) ) { const response = await fetch ( file . download_url ) ; const contents = await response . text ( ) ; try { const yamlContents = yaml . load ( contents ) ; const strings = yamlContents . translations ; const context = file . name . replace ( './' , '' ) . replace ( '.yml' , '' ) ; locale . stringFiles . push ( { strings , url : file . download_url , context } ) ; } catch ( error ) { console . log ( `// Error loading file ${ file . name }` ) ; console . log ( error ) ; } } } locales . push ( locale ) ; } return locales ; } ; export const loadLocally = async ( localesWithRepos ) => { let i = 0 ; for ( const locale of localesWithRepos ) { i ++ ; console . log ( `-> loading directory ${ locale . repo } locally (${ i }/${ localesWithRepos . length })` ) ; locale . stringFiles = [ ] ; const [ owner , repo ] = locale . repo . split ( '/' ) ; const devDir = __dirname . split ( '/' ) . slice ( 1 , - 2 ) . join ( '/' ) ; const path = `/${ devDir }/stateof-locales/${ repo }` ; const files = await readdir ( path ) ; const yamlFiles = files . filter ( ( f ) => f . includes ( '.yml' ) ) ; for ( const fileName of yamlFiles ) { const filePath = path + '/' + fileName ; const contents = await readFile ( filePath , 'utf8' ) ; const yamlContents = yaml . load ( contents ) ; const strings = yamlContents . translations ; const context = fileName . replace ( './' , '' ) . replace ( '.yml' , '' ) ; locale . stringFiles . push ( { strings , url : filePath , context } ) ; } locales . push ( locale ) ; } return locales ; } ; export const loadLocales = async ( ) => { console . log ( '// loading locales…' ) ; const localesWithRepos = localesYAML . filter ( ( locale ) => ! ! locale . repo ) ; const locales = process . env . LOAD_LOCALES === 'local' ? await loadLocally ( localesWithRepos ) : await loadFromGitHub ( localesWithRepos ) ; console . log ( '// done loading locales' ) ; return locales ; } ; /**
 * Return either e.g. other_tools.browsers.choices or other_tools.browsers.others_normalized
 */ export const getOtherKey = ( id ) => id . includes ( '_others' ) ? `${ id . replace ( '_others' , '' ) }.others_normalized` : `${ id }.choices` ; export const getGraphQLEnumValues = ( name ) : string [ ] => { const enumDef = typeDefs . definitions . find ( def => { return def . kind === 'EnumTypeDefinition' && def . name . value === name ; } ) as EnumTypeDefinitionNode ; if ( enumDef === undefined ) { throw new Error ( `No enum found matching name: ${ name }` ) ; } return enumDef . values ! . map ( v => v . name . value ) ; } ; export const truncateKey = ( key ) => key . split ( '-' ) [ 0 ] ; export const getValidLocale = async ( localeId ) => { const locales = await loadOrGetLocales ( ) ; const exactLocale = locales . find ( ( locale ) => locale . id . toLowerCase ( ) === localeId . toLowerCase ( ) ) ; const similarLocale = locales . find ( ( locale ) => truncateKey ( locale . id ) === truncateKey ( localeId ) ) ; return exactLocale || similarLocale ; } ; export const getLocaleStrings = ( locale , contexts ? ) => { let stringFiles = locale . stringFiles ; if ( contexts ) { stringFiles = stringFiles . filter ( ( sf ) => { return contexts . includes ( sf . context ) ; } ) ; } const strings = stringFiles . map ( ( sf ) => { let { strings , prefix , context } = sf ; if ( strings === null ) { return [ ] ; } if ( prefix ) { strings = strings . map ( ( s ) => ( { ... s , key : `${ prefix }.${ s . key }` } ) ) ; } strings = strings . map ( ( s ) => ( { ... s , context } ) ) ; strings = strings . map ( ( s ) => ( { ... s , tHtml : marked ( String ( s . t ) ) } ) ) ; return strings ; } ) . flat ( ) ; return { strings } ; } ; export const getLocaleStringsWithFallback = async ( locale , contexts ? ) => { let localeStrings = [ ] , translatedCount = 0 , totalCount = 0 , untranslatedKeys = [ ] ; const enLocale = await getValidLocale ( 'en-US' ) ; if ( enLocale ) { const enStrings = getLocaleStrings ( enLocale , contexts ) . strings ; if ( locale . id === 'en-US' ) { return { strings : enStrings . map ( ( t ) => ( { ... t , fallback : false } ) ) , translatedCount : enStrings . length , totalCount : enStrings . length , completion : 100 , untranslatedKeys } ; } localeStrings = getLocaleStrings ( locale , contexts ) . strings ; enStrings . forEach ( ( enTranslation ) => { totalCount ++ ; const localeTranslationIndex = localeStrings . findIndex ( t => t . key === enTranslation . key && ! t . fallback ) ; if ( localeTranslationIndex === - 1 || localeStrings [ localeTranslationIndex ] . t === enTranslation . t || ( localeStrings [ localeTranslationIndex ] . t && localeStrings [ localeTranslationIndex ] . t . trim ( ) === 'TODO' ) ) { localeStrings . push ( { ... enTranslation , fallback : true } ) ; untranslatedKeys . push ( enTranslation . key ) ; } else { translatedCount ++ ; localeStrings [ localeTranslationIndex ] . fallback = false ; } } ) ; } return { strings : localeStrings , translatedCount , totalCount , completion : Math . round ( ( translatedCount * 100 ) / totalCount ) , untranslatedKeys } ; } ; export const getLocaleObject = async ( localeId , contexts ? , enableFallbacks = true ) => { const validLocale = await getValidLocale ( localeId ) ; if ( ! validLocale ) { throw new Error ( `No locale found for key ${ localeId }` ) ; } const localeData = enableFallbacks ? await getLocaleStringsWithFallback ( validLocale , contexts ) : await getLocaleStrings ( validLocale , contexts ) ; return { ... validLocale , ... localeData } ; } ; export const getLocales = async ( contexts ? , enableFallbacks ? ) => { const rawLocales = await loadOrGetLocales ( ) ; const locales = [ ] ; for ( const locale of rawLocales ) { const localeObject = await getLocaleObject ( locale . id , contexts , enableFallbacks ) ; locales . push ( localeObject ) ; } return locales ; } ; export const getTranslation = async ( key , localeId ) => { const locale = await getLocaleObject ( localeId ) ; return locale . strings . reverse ( ) . find ( ( s ) => s . key === key ) ; } ; export const initLocales = async ( ) => { console . log ( '// initializing locales…' ) ; const rawLocales = await loadOrGetLocales ( ) ; logToFile ( 'raw_locales.json' , rawLocales , { mode : 'overwrite' } ) ; const parsedLocales = await getLocales ( ) ; logToFile ( 'parsed_locales.json' , parsedLocales , { mode : 'overwrite' } ) ; } ;