using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using UnityEditor; using UnityEditor.Localization; using UnityEngine; using Object = UnityEngine.Object; namespace Crowdin.Plugin.Operations { using Helpers; using Helpers.FileHelpers; using Helpers.PluginLocalization; using Helpers.UnityLocalization; using Models; using Models.Translations; using Models.Translations.Items; using Server_API; using Server_API.Helpers; using Server_API.Serializable.Build; using Server_API.Serializable.Directories; using Server_API.Services; /// Pulls asset translations from Crowdin: triggers a directory build, polls it, downloads and /// unpacks the resulting zip, then imports the assets into the local Unity Localization tables. public static class PullAssets { private static readonly string ZipPath = $"{FolderHelper.resourcesDirectory}/DownloadTranslations.zip"; public static async Task RunAsync( PullOptions options, IProgress progress, CancellationToken cancellationToken) { progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey(LocalKeys.pulling_from_server))); ClearDownloadFiles(); var service = new TranslationsApiService(); BuildData build = null; try { int directoryId = await FindProjectDirectoryId(progress, cancellationToken); build = await service.BuildDirectoryTranslation(directoryId, cancellationToken); int? lastLoggedProgress = null; while (true) { BuildStatusData status = await service.GetBuildStatus(build.id, cancellationToken); if (status.progress != lastLoggedProgress) { lastLoggedProgress = status.progress; progress.Report(new OperationProgress( LocalizationHelper.GetLocalStringByKey(LocalKeys.build_status, status.progress.ToString()), status.progress / 100f)); } if (status.status is "finished") { break; } await Task.Delay(500, cancellationToken); } BuildFileData buildFile = await service.GetBuildDownloadUrl(build.id, cancellationToken); progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey(LocalKeys.on_build_translation))); byte[] zipBytes = await CrowdinHttpClient.Default.DownloadAsync(buildFile.url, cancellationToken); File.WriteAllBytes(ZipPath, zipBytes); progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey(LocalKeys.on_download_source_file))); ParseAssetsFromZip(options.Tables, ZipPath, options.PreserveEntryIds); progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey(LocalKeys.unzip_done, ZipPath))); } catch (OperationCanceledException) { // Cancelling here only stops this client from polling - the build Crowdin started // keeps running server-side and blocks the next pull with a 409 "Another build is // currently in progress" until it finishes on its own. Tell Crowdin to cancel it // too, using a fresh token since cancellationToken is already signalled. if (build is not null) { try { await service.DeleteBuild(build.id, CancellationToken.None); } catch (CrowdinApiException ex) { Debug.LogException(ex); } } throw; } finally { ClearDownloadFiles(); } int receivedCount = TranslationsHelper.GetTranslationAssets() .Count(asset => options.Tables.Contains(asset.exportedFromTable)); progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey(LocalKeys.pull_successful))); AssetDatabase.Refresh(); Dictionary importedPerTable = ImportPulledAssets(options.Tables, progress); progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey( LocalKeys.stats_received_asset_translations, receivedCount.ToString()))); return new PullResult(receivedCount, importedPerTable); } private static async Task FindProjectDirectoryId(IProgress progress, CancellationToken cancellationToken) { var service = new DirectoriesApiService(); List directories = await service.ListDirectories(0, DirectoriesHelper.RootDirectoryName, cancellationToken); DirectoryObj directory = directories .FirstOrDefault(data => data.name is DirectoriesHelper.RootDirectoryName); if (directory is null) { throw new CrowdinApiException( (HttpStatusCode)0, LocalizationHelper.GetLocalStringByKey(LocalKeys.directory_not_found)); } progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey(LocalKeys.directory_found))); return directory.id; } /// Unpacks the build zip and imports every asset table's translations into TranslationsHelper's /// local cache. Pure and network-free so it can run against a pre-downloaded zip in tests. internal static void ParseAssetsFromZip(IReadOnlyList tables, string zipPath, bool preserveEntryIds = false) { FolderHelper.TryCreateFolders(); DecompressZipHelper.UnZipPackage(zipPath); ClearAssetsFolder(); foreach (string tableName in tables) { ParseTranslationAssets(tableName, preserveEntryIds); } } private static void ParseTranslationAssets(string tableName, bool preserveEntryIds) { var tempDictionary = new Dictionary>(); foreach (var languageItem in SettingsHelper.GetLanguageScheme()) { var downloadPath = $"{FolderHelper.downloadDirectory}/{languageItem.langType}/{tableName}"; var tableCopy = $"{FolderHelper.AssetsDirectory}/{tableName}"; var copyPath = $"{tableCopy}/{languageItem.langType}"; if (!Directory.Exists(downloadPath)) { continue; } if (!Directory.Exists(tableCopy)) { Directory.CreateDirectory(tableCopy); } if (!Directory.Exists(copyPath)) { FileUtil.CopyFileOrDirectory(downloadPath, copyPath); } AssetDatabase.Refresh(); foreach (var file in Directory.GetFiles(copyPath)) { if (Path.GetExtension(file) is ".meta") { continue; } var filename = Path.GetFileNameWithoutExtension(file); if (!tempDictionary.ContainsKey(filename)) { tempDictionary.Add(filename, new Dictionary()); } if (AssetImporter.GetAtPath(file) is TextureImporter textureImporter) { textureImporter.textureType = TextureImporterType.Sprite; } AssetDatabase.ImportAsset(file); AssetDatabase.Refresh(); var asset = AssetDatabase.LoadAssetAtPath(file); tempDictionary[filename].Add(languageItem.langType, asset); } } foreach (var valuePair in tempDictionary) { var translations = new List(); foreach (var pair in valuePair.Value) { translations.Add(new TranslationAssetItem { language = pair.Key, asset = pair.Value }); } var translation = new TranslationAssetObj(valuePair.Key, translations, tableName); TranslationsHelper.TryAddOrUpdateTranslation(translation, preserveEntryIds); } } private static void ClearAssetsFolder() { RemoveFileHelper.RemoveDirectory(FolderHelper.AssetsDirectory); Directory.CreateDirectory(FolderHelper.AssetsDirectory); } private static void ClearDownloadFiles() { RemoveFileHelper.RemoveFile(ZipPath); RemoveFileHelper.RemoveDirectory(FolderHelper.downloadDirectory); } private static Dictionary ImportPulledAssets(IReadOnlyList tables, IProgress progress) { progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey(LocalKeys.start_import_assets))); var perTable = new Dictionary(); IEnumerable translationAssets = TranslationsHelper.GetTranslationAssets() .Where(asset => tables.Contains(asset.exportedFromTable)); foreach (TranslationAsset asset in translationAssets) { if (ImportDataHelper.ImportAssetRow(asset, asset.exportedFromTable)) { progress.Report(new OperationProgress( LocalizationHelper.GetLocalStringByKey(LocalKeys.import_asset, asset.key))); perTable[asset.exportedFromTable] = perTable.GetValueOrDefault(asset.exportedFromTable) + 1; } } progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey( LocalKeys.import_comp_asset_translations, perTable.Count.ToString(), perTable.Values.Sum().ToString()))); foreach ((string table, int count) in perTable) { progress.Report(new OperationProgress(LocalizationHelper.GetLocalStringByKey( LocalKeys.import_item_asset, table, count.ToString()))); } foreach (string tableName in tables) { var tableCollection = LocalizationEditorSettings.GetAssetTableCollection(tableName); EditorUtility.SetDirty(tableCollection.SharedData); EditorUtility.SetDirty(tableCollection); foreach (var assetTable in tableCollection.AssetTables) { EditorUtility.SetDirty(assetTable); } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); return perTable; } } }