Skip to content

Commit

Permalink
Add support for exporting and importing from xcode project
Browse files Browse the repository at this point in the history
  • Loading branch information
icodesign committed Aug 31, 2024
1 parent f2353ab commit 544aec0
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 124 deletions.
44 changes: 34 additions & 10 deletions apps/cli/src/commands/core.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Config, parseConfig } from '@repo/base/config';
import { Config, LocalizationFormat, parseConfig } from '@repo/base/config';
import { consoleLogger, logger } from '@repo/base/logger';
import spinner from '@repo/base/spinner';
import { createTemporaryOutputFolder, replaceBundle } from '@repo/ioloc';
import {
ExportLocalizationsResult,
LocalizationBundlePath,
exportLocalizationBundle,
importLocalizationBundle,
textHash,
Expand Down Expand Up @@ -38,9 +38,14 @@ export async function loadConfig({ path }: { path?: string }) {

export async function exportLocalizations(config: Config) {
var startTime = performance.now();
spinner.update('Exporting localizations').start();
const message =
config.localizations.filter((x) => x.format === LocalizationFormat.XCODE)
.length > 0
? `Exporting localizations (Xcode project may take a while)`
: 'Exporting localizations';
spinner.update(message).start();
const baseFolder = path.dirname(config.path);
var exportedResults: ExportLocalizationsResult[] = [];
var exportedResults: LocalizationBundlePath[] = [];
let baseOutputFolder = config.exportFolder || '.dolphin';
if (!path.isAbsolute(baseOutputFolder)) {
baseOutputFolder = path.join(baseFolder, baseOutputFolder);
Expand Down Expand Up @@ -81,7 +86,12 @@ export async function exportLocalizations(config: Config) {
.map((result) => result.bundlePath)
.join(', ')}`,
);
return baseOutputFolder;
return {
baseOutputFolder,
intermediateBundlePaths: exportedResults.map(
(x) => x.intermediateBundlePath,
),
};
}

export async function translateLocalizations({
Expand All @@ -104,7 +114,7 @@ export async function translateLocalizations({
const translated = translationResult.mergedStrings;
const translatedCount = Object.keys(translated).length;
if (translatedCount === 0) {
spinner.succeed(chalk.green('No string needs to be translated'));
spinner.succeed(chalk.green('No string needs to be translated\n'));
return;
}
const duration = formattedDuration(performance.now() - startTime);
Expand All @@ -128,20 +138,34 @@ export async function importLocalizations({
translationBundle,
}: {
config: Config;
translationBundle: string;
translationBundle: {
baseOutputFolder: string;
intermediateBundlePaths: (string | undefined)[];
};
}) {
const startTime = performance.now();
spinner.next('Merging translations').start();
const containsXcodeFormat =
config.localizations.filter((x) => x.format === LocalizationFormat.XCODE)
.length > 0;
let message = 'Merging translations';
if (containsXcodeFormat) {
message += ' (Xcode project may take a while)';
}
spinner.next(message).start();
logger.info(`Merging localization bundles...`);
for (var index = 0; index < config.localizations.length; index++) {
const localizationConfig = config.localizations[index];
const bundlePath = path.join(
translationBundle,
translationBundle.baseOutputFolder,
localizationFolder(localizationConfig.id),
);
await importLocalizationBundle({
config: localizationConfig,
localizationBundlePath: bundlePath,
localizationBundlePath: {
bundlePath,
intermediateBundlePath:
translationBundle.intermediateBundlePaths[index],
},
baseLanguage: config.baseLanguage,
baseFolder: path.dirname(config.path),
});
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async function handleLocalizeCommand(args: CmdArgs) {
});
const translationBundle = await exportLocalizations(config);
await translateLocalizations({
baseOutputFolder: translationBundle,
baseOutputFolder: translationBundle.baseOutputFolder,
config,
});
await importLocalizations({
Expand Down
81 changes: 77 additions & 4 deletions packages/ioloc/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { logger } from '@repo/base/logger';
import fs from 'node:fs';
import path from 'node:path';

import { ExportLocalizations, ExportLocalizationsResult } from '../index.js';
import { createOutputFolderIfNeed } from '../utils.js';
import { ExportLocalizations, LocalizationBundlePath } from '../index.js';
import {
createOutputFolderIfNeed,
createTemporaryOutputFolder,
} from '../utils.js';
import { XcodeExportLocalizations } from '../xcode.js';
import { stringifyXliff2 } from '../xliff/index.js';
import { Xliff } from '../xliff/xliff-spec.js';
import { XliffParser } from './parser/xliff.js';
import { XlocParser } from './parser/xloc.js';

export * from './parser/text.js';
export * from './parser/strings.js';
Expand Down Expand Up @@ -44,14 +50,14 @@ export class BasicExporter implements ExportLocalizations {
}: {
config: ExportConfig;
parser: ExportParser;
outputFolder: string;
outputFolder?: string;
}) {
this.config = config;
this.parser = parser;
this.outputFolder = outputFolder;
}

async export(): Promise<ExportLocalizationsResult> {
async export(): Promise<LocalizationBundlePath> {
let sourcePath = this.config.sourceLanguage.path;
const outputFolder = await createOutputFolderIfNeed(this.outputFolder);
for (const langConfig of this.config.targetLanguages) {
Expand All @@ -74,3 +80,70 @@ export class BasicExporter implements ExportLocalizations {
};
}
}

export class XcodeExporter implements ExportLocalizations {
private config: ExportConfig;
private projectPath: string;
private outputFolder?: string;

constructor({
config,
projectPath,
outputFolder,
}: {
config: ExportConfig;
projectPath: string;
outputFolder: string;
}) {
this.config = config;
this.projectPath = projectPath;
this.outputFolder = outputFolder;
}

async export(): Promise<LocalizationBundlePath> {
// For xcode, we need export localizations first
const xcodeOutputFolder = await createTemporaryOutputFolder();
logger.info(`Exporting Xcode project to ${xcodeOutputFolder}`);
const xcodeExporter = new XcodeExportLocalizations(
this.projectPath,
xcodeOutputFolder,
);
const result = await xcodeExporter.export();
// const result = {
// bundlePath:
// '/var/folders/x3/d3jx55kn439_kdfysr655ld00000gn/T/dolphin-export-mjIgGq/',
// languages: ['en', 'zh-Hans', 'ko', 'ja'],
// };
logger.info(
`Exported Xcode project at ${result.bundlePath}, languages: ${result.languages}`,
);
const exportConfig = {
sourceLanguage: {
code: this.config.sourceLanguage.code,
path: path.join(
result.bundlePath,
`${this.config.sourceLanguage.code}.xcloc`,
),
},
targetLanguages: result.languages.map((language: string) => {
let bundlePath = path.join(result.bundlePath, `${language}.xcloc`);
if (!path.isAbsolute(bundlePath)) {
bundlePath = path.join(this.config.basePath, bundlePath);
}
return {
code: language,
path: bundlePath,
};
}),
basePath: this.config.basePath,
};
let basicExporter = new BasicExporter({
config: exportConfig,
parser: new XlocParser(),
outputFolder: this.outputFolder,
});
const exportResult = await basicExporter.export();
exportResult.intermediateBundlePath = result.bundlePath;
return exportResult;
}
}
81 changes: 80 additions & 1 deletion packages/ioloc/src/import/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { logger } from '@repo/base/logger';
import fs from 'node:fs';
import path from 'node:path';

import { ImportLocalizations, ImportLocalizationsResult } from '../index.js';
import {
ImportLocalizations,
ImportLocalizationsResult,
LocalizationBundlePath,
} from '../index.js';
import { XcodeImportLocalizations } from '../xcode.js';
import { parseXliff2Text } from '../xliff/index.js';
import { Xliff } from '../xliff/xliff-spec.js';
import { XclocMerger } from './merger/xcloc.js';

export * from './merger/text.js';
export * from './merger/xliff.js';
Expand Down Expand Up @@ -70,3 +77,75 @@ export class BasicImporter implements ImportLocalizations {
};
}
}

export class XcodeImporter implements ImportLocalizations {
private config: ImportConfig;
private localizationBundlePath: LocalizationBundlePath;
private projectPath: string;
private baseFolder: string;

constructor({
config,
projectPath,
baseFolder,
localizationBundlePath,
}: {
config: ImportConfig;
projectPath: string;
baseFolder: string;
localizationBundlePath: LocalizationBundlePath;
}) {
this.config = config;
this.localizationBundlePath = localizationBundlePath;
this.projectPath = projectPath;
this.baseFolder = baseFolder;
}

async import(): Promise<ImportLocalizationsResult> {
// Step 1: Use BasicImporter to localize strings to intermediateBundlePath
const intermediateBundlePath =
this.localizationBundlePath.intermediateBundlePath;
if (!intermediateBundlePath) {
throw new Error(
'intermediateBundlePath is not set for importing xcode project',
);
}
let importBundlePath = this.localizationBundlePath.bundlePath;
if (!path.isAbsolute(importBundlePath)) {
importBundlePath = path.join(this.baseFolder, importBundlePath);
}
const basicImporter = new BasicImporter({
config: {
sourceLanguage: {
code: this.config.sourceLanguage.code,
path: path.join(
intermediateBundlePath,
`${this.config.sourceLanguage.code}.xcloc`,
),
},
targetLanguages: this.config.targetLanguages.map((lang) => ({
...lang,
to: path.join(intermediateBundlePath, `${lang.code}.xcloc`),
})),
},
merger: new XclocMerger(),
});

const basicImportResult = await basicImporter.import();
if (basicImportResult.code !== 0) {
return basicImportResult;
}

// Step 2: Use XcodeImportLocalizations to import intermediateBundlePath to Xcode project
const xcodeImporter = new XcodeImportLocalizations();

const xcodeImportResult = await xcodeImporter.import({
localizationBundlePath: intermediateBundlePath,
projectPath: this.projectPath,
baseFolder: this.baseFolder,
});

logger.info('Xcode import completed');
return xcodeImportResult;
}
}
Loading

0 comments on commit 544aec0

Please sign in to comment.