1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types'; 2082815dcSEvan Baconimport JsonFile from '@expo/json-file'; 3082815dcSEvan Baconimport fs from 'fs'; 4082815dcSEvan Baconimport { join, relative } from 'path'; 5082815dcSEvan Baconimport { XcodeProject } from 'xcode'; 6082815dcSEvan Bacon 7*8a424bebSJames Ideimport { addResourceFileToGroup, ensureGroupRecursively, getProjectName } from './utils/Xcodeproj'; 8082815dcSEvan Baconimport { ConfigPlugin } from '../Plugin.types'; 9082815dcSEvan Baconimport { withXcodeProject } from '../plugins/ios-plugins'; 10082815dcSEvan Baconimport { addWarningIOS } from '../utils/warnings'; 11082815dcSEvan Bacon 12082815dcSEvan Bacontype LocaleJson = Record<string, string>; 13082815dcSEvan Bacontype ResolvedLocalesJson = Record<string, LocaleJson>; 14082815dcSEvan Bacontype ExpoConfigLocales = NonNullable<ExpoConfig['locales']>; 15082815dcSEvan Bacon 16082815dcSEvan Baconexport const withLocales: ConfigPlugin = (config) => { 17082815dcSEvan Bacon return withXcodeProject(config, async (config) => { 18082815dcSEvan Bacon config.modResults = await setLocalesAsync(config, { 19082815dcSEvan Bacon projectRoot: config.modRequest.projectRoot, 20082815dcSEvan Bacon project: config.modResults, 21082815dcSEvan Bacon }); 22082815dcSEvan Bacon return config; 23082815dcSEvan Bacon }); 24082815dcSEvan Bacon}; 25082815dcSEvan Bacon 26082815dcSEvan Baconexport function getLocales( 27082815dcSEvan Bacon config: Pick<ExpoConfig, 'locales'> 28082815dcSEvan Bacon): Record<string, string | LocaleJson> | null { 29082815dcSEvan Bacon return config.locales ?? null; 30082815dcSEvan Bacon} 31082815dcSEvan Bacon 32082815dcSEvan Baconexport async function setLocalesAsync( 33082815dcSEvan Bacon config: Pick<ExpoConfig, 'locales'>, 34082815dcSEvan Bacon { projectRoot, project }: { projectRoot: string; project: XcodeProject } 35082815dcSEvan Bacon): Promise<XcodeProject> { 36082815dcSEvan Bacon const locales = getLocales(config); 37082815dcSEvan Bacon if (!locales) { 38082815dcSEvan Bacon return project; 39082815dcSEvan Bacon } 40082815dcSEvan Bacon // possibly validate CFBundleAllowMixedLocalizations is enabled 41082815dcSEvan Bacon const localesMap = await getResolvedLocalesAsync(projectRoot, locales); 42082815dcSEvan Bacon 43082815dcSEvan Bacon const projectName = getProjectName(projectRoot); 44082815dcSEvan Bacon const supportingDirectory = join(projectRoot, 'ios', projectName, 'Supporting'); 45082815dcSEvan Bacon 46082815dcSEvan Bacon // TODO: Should we delete all before running? Revisit after we land on a lock file. 47082815dcSEvan Bacon const stringName = 'InfoPlist.strings'; 48082815dcSEvan Bacon 49082815dcSEvan Bacon for (const [lang, localizationObj] of Object.entries(localesMap)) { 50082815dcSEvan Bacon const dir = join(supportingDirectory, `${lang}.lproj`); 51082815dcSEvan Bacon // await fs.ensureDir(dir); 52082815dcSEvan Bacon await fs.promises.mkdir(dir, { recursive: true }); 53082815dcSEvan Bacon 54082815dcSEvan Bacon const strings = join(dir, stringName); 55082815dcSEvan Bacon const buffer = []; 56082815dcSEvan Bacon for (const [plistKey, localVersion] of Object.entries(localizationObj)) { 57082815dcSEvan Bacon buffer.push(`${plistKey} = "${localVersion}";`); 58082815dcSEvan Bacon } 59082815dcSEvan Bacon // Write the file to the file system. 60082815dcSEvan Bacon await fs.promises.writeFile(strings, buffer.join('\n')); 61082815dcSEvan Bacon 62082815dcSEvan Bacon const groupName = `${projectName}/Supporting/${lang}.lproj`; 63082815dcSEvan Bacon // deep find the correct folder 64082815dcSEvan Bacon const group = ensureGroupRecursively(project, groupName); 65082815dcSEvan Bacon 66082815dcSEvan Bacon // Ensure the file doesn't already exist 67082815dcSEvan Bacon if (!group?.children.some(({ comment }) => comment === stringName)) { 68082815dcSEvan Bacon // Only write the file if it doesn't already exist. 69082815dcSEvan Bacon project = addResourceFileToGroup({ 70082815dcSEvan Bacon filepath: relative(supportingDirectory, strings), 71082815dcSEvan Bacon groupName, 72082815dcSEvan Bacon project, 73082815dcSEvan Bacon isBuildFile: true, 74082815dcSEvan Bacon verbose: true, 75082815dcSEvan Bacon }); 76082815dcSEvan Bacon } 77082815dcSEvan Bacon } 78082815dcSEvan Bacon 79082815dcSEvan Bacon return project; 80082815dcSEvan Bacon} 81082815dcSEvan Bacon 82082815dcSEvan Baconexport async function getResolvedLocalesAsync( 83082815dcSEvan Bacon projectRoot: string, 84082815dcSEvan Bacon input: ExpoConfigLocales 85082815dcSEvan Bacon): Promise<ResolvedLocalesJson> { 86082815dcSEvan Bacon const locales: ResolvedLocalesJson = {}; 87082815dcSEvan Bacon for (const [lang, localeJsonPath] of Object.entries(input)) { 88082815dcSEvan Bacon if (typeof localeJsonPath === 'string') { 89082815dcSEvan Bacon try { 90082815dcSEvan Bacon locales[lang] = await JsonFile.readAsync(join(projectRoot, localeJsonPath)); 91082815dcSEvan Bacon } catch { 92082815dcSEvan Bacon // Add a warning when a json file cannot be parsed. 93082815dcSEvan Bacon addWarningIOS( 94082815dcSEvan Bacon `locales.${lang}`, 95082815dcSEvan Bacon `Failed to parse JSON of locale file for language: ${lang}`, 96082815dcSEvan Bacon 'https://docs.expo.dev/distribution/app-stores/#localizing-your-ios-app' 97082815dcSEvan Bacon ); 98082815dcSEvan Bacon } 99082815dcSEvan Bacon } else { 100082815dcSEvan Bacon // In the off chance that someone defined the locales json in the config, pass it directly to the object. 101082815dcSEvan Bacon // We do this to make the types more elegant. 102082815dcSEvan Bacon locales[lang] = localeJsonPath; 103082815dcSEvan Bacon } 104082815dcSEvan Bacon } 105082815dcSEvan Bacon 106082815dcSEvan Bacon return locales; 107082815dcSEvan Bacon} 108