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