Skip to content

Commit 18cc64f

Browse files
committed
Add missing translations at build time
1 parent a9aec79 commit 18cc64f

File tree

4 files changed

+86
-65
lines changed

4 files changed

+86
-65
lines changed

demo/app/i18n/fr.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"hello": {
3+
"world": "Bonjour le monde !"
4+
}
5+
}

hooks/converter.android.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
import * as fs from "fs";
22
import * as path from "path";
33

4-
import { ConverterCommon, I18nEntry, SupportedLanguages } from "./converter.common";
4+
import { ConverterCommon, I18nEntries, SupportedLanguages } from "./converter.common";
55
import { encodeKey, encodeValue } from "../src/resource.android";
66

77
export class ConverterAndroid extends ConverterCommon {
88
protected cleanObsoleteResourcesFiles(resourcesDirectory: string, supportedLanguages: SupportedLanguages): this {
99
fs.readdirSync(resourcesDirectory).filter(fileName => {
10-
const match = /^values(?:-(.+))?$/.exec(fileName);
11-
if (!match) {
12-
return false;
13-
} else if (match[1]) {
14-
return !supportedLanguages.has(match[1]);
15-
} else {
16-
for (const [language, isDefaultLanguage] of supportedLanguages) {
17-
if (isDefaultLanguage) { return false; }
18-
}
19-
return true;
20-
}
10+
const match = /^values-(.+)$/.exec(fileName);
11+
return match && !supportedLanguages.has(match[1]);
2112
}).map(fileName => {
2213
return path.join(resourcesDirectory, fileName);
2314
}).filter(filePath => {
@@ -35,23 +26,23 @@ export class ConverterAndroid extends ConverterCommon {
3526
protected createLanguageResourcesFiles(
3627
language: string,
3728
isDefaultLanguage: boolean,
38-
i18nContentIterator: Iterable<I18nEntry>
29+
i18nEntries: I18nEntries
3930
): this {
4031
const languageResourcesDir = path.join(
4132
this.appResourcesDirectoryPath,
4233
`values${isDefaultLanguage ? "" : `-${language}`}`
4334
);
4435
this.createDirectoryIfNeeded(languageResourcesDir);
4536
let strings = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n";
46-
for (const { key, value } of i18nContentIterator) {
37+
i18nEntries.forEach((value, key) => {
4738
const encodedKey = encodeKey(key);
4839
const encodedValue = encodeValue(value);
4940
strings += ` <string name="${encodedKey}">${encodedValue}</string>\n`;
5041
if (key === "app.name") {
5142
strings += ` <string name="app_name">${encodedValue}</string>\n`;
5243
strings += ` <string name="title_activity_kimera">${encodedValue}</string>\n`;
5344
}
54-
}
45+
});
5546
strings += "</resources>\n";
5647
const resourceFilePath = path.join(languageResourcesDir, "strings.xml");
5748
if (this.writeFileSyncIfNeeded(resourceFilePath, strings)) {

hooks/converter.common.ts

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,36 +36,79 @@ export abstract class ConverterCommon extends EventEmitter {
3636
protected abstract createLanguageResourcesFiles(
3737
language: string,
3838
isDefaultLanguage: boolean,
39-
i18nContentIterator: Iterable<I18nEntry>
39+
i18nEntries: I18nEntries
4040
): this;
4141

4242
public abstract livesyncExclusionPatterns(): string[];
4343

44+
public loadLangage(filePath: string): I18nEntries {
45+
delete (<any>require).cache[(<any>require).resolve(filePath)];
46+
47+
const fileContent = require(filePath);
48+
const i18nEntries: I18nEntries = new Map();
49+
const stack = [{ prefix: "", element: fileContent }];
50+
51+
while (stack.length > 0) {
52+
const { prefix, element } = stack.pop();
53+
if (Array.isArray(element)) {
54+
i18nEntries.set(prefix, element.join(""));
55+
} else if (typeof element === "object") {
56+
for (const key of Object.keys(element)) {
57+
stack.push({ prefix: prefix === "" ? key : `${prefix}.${key}`, element: element[key] });
58+
}
59+
} else {
60+
i18nEntries.set(prefix, new String(element).valueOf());
61+
}
62+
}
63+
64+
return i18nEntries;
65+
}
66+
4467
public run(): this {
4568
if (!fs.existsSync(this.i18nDirectoryPath)) {
46-
this.logger.info(`'${this.i18nDirectoryPath}' doesn't exists: nothing to localize`);
69+
this.logger.warn(`'${this.i18nDirectoryPath}' doesn't exists: nothing to localize`);
4770
return this;
4871
}
4972

73+
let defaultLanguage = undefined;
5074
const supportedLanguages: SupportedLanguages = new Map();
5175

5276
fs.readdirSync(this.i18nDirectoryPath).map(fileName => {
5377
return path.join(this.i18nDirectoryPath, fileName);
5478
}).filter(filePath => {
5579
return fs.statSync(filePath).isFile();
56-
}).map(filePath => {
57-
delete (<any>require).cache[(<any>require).resolve(filePath)];
58-
return filePath;
59-
}).map(filePath => {
80+
}).forEach(filePath => {
6081
let language = path.basename(filePath, path.extname(filePath));
61-
const isDefaultLanguage = path.extname(language) === ".default";
62-
if (isDefaultLanguage) { language = path.basename(language, ".default"); }
63-
supportedLanguages.set(language, isDefaultLanguage);
64-
this.createLanguageResourcesFiles(
65-
language,
66-
isDefaultLanguage,
67-
this.i18nContentGenerator(require(filePath))
68-
);
82+
if (path.extname(language) === ".default") {
83+
language = path.basename(language, ".default");
84+
defaultLanguage = language;
85+
}
86+
supportedLanguages.set(language, filePath);
87+
});
88+
89+
if (supportedLanguages.size === 0) {
90+
this.logger.warn(`'${this.i18nDirectoryPath}' is empty: nothing to localize`);
91+
return this;
92+
}
93+
94+
if (!defaultLanguage) {
95+
defaultLanguage = supportedLanguages.keys().next().value;
96+
this.logger.warn(`No file found with the .default extension: default langage set to '${defaultLanguage}'`);
97+
}
98+
99+
const defaultLanguageI18nEntries = this.loadLangage(supportedLanguages.get(defaultLanguage));
100+
this.createLanguageResourcesFiles(defaultLanguage, true, defaultLanguageI18nEntries);
101+
102+
supportedLanguages.forEach((filePath, language) => {
103+
if (language !== defaultLanguage) {
104+
const languageI18nEntries = this.loadLangage(filePath);
105+
defaultLanguageI18nEntries.forEach((value, key) => {
106+
if (!languageI18nEntries.has(key)) {
107+
languageI18nEntries.set(key, value);
108+
}
109+
});
110+
this.createLanguageResourcesFiles(language, false, languageI18nEntries);
111+
}
69112
});
70113

71114
[this.appResourcesDirectoryPath, this.appResourcesDestinationDirectoryPath].forEach(resourcesDirectoryPath => {
@@ -77,22 +120,6 @@ export abstract class ConverterCommon extends EventEmitter {
77120
return this;
78121
}
79122

80-
public * i18nContentGenerator(i18nContent: any): Iterable<I18nEntry> {
81-
const stack = [{ prefix: "", element: i18nContent }];
82-
while (stack.length > 0) {
83-
const { prefix, element } = stack.pop();
84-
if (Array.isArray(element)) {
85-
yield { key: prefix, value: element.join("") };
86-
} else if (typeof element === "object") {
87-
for (const key of Object.keys(element)) {
88-
stack.push({ prefix: prefix === "" ? key : `${prefix}.${key}`, element: element[key] });
89-
}
90-
} else {
91-
yield { key: <string>prefix, value: new String(element).valueOf() };
92-
}
93-
}
94-
}
95-
96123
protected createDirectoryIfNeeded(directoryPath: string): this {
97124
if (!fs.existsSync(directoryPath) || !fs.statSync(directoryPath).isDirectory()) { mkdirp.sync(directoryPath); }
98125
return this;
@@ -118,5 +145,5 @@ export abstract class ConverterCommon extends EventEmitter {
118145
}
119146
}
120147

121-
export type I18nEntry = { key: string; value: string; };
122-
export type SupportedLanguages = Map<string, boolean>;
148+
export type I18nEntries = Map<string, string>;
149+
export type SupportedLanguages = Map<string, string>;

hooks/converter.ios.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from "fs";
22
import * as path from "path";
33
import * as plist from "simple-plist";
44

5-
import { ConverterCommon, I18nEntry, SupportedLanguages } from "./converter.common";
5+
import { ConverterCommon, I18nEntries, SupportedLanguages } from "./converter.common";
66
import { encodeKey, encodeValue } from "../src/resource.ios";
77

88
export class ConverterIOS extends ConverterCommon {
@@ -30,27 +30,25 @@ export class ConverterIOS extends ConverterCommon {
3030
protected createLanguageResourcesFiles(
3131
language: string,
3232
isDefaultLanguage: boolean,
33-
i18nContentIterator: Iterable<I18nEntry>
33+
i18nEntries: I18nEntries
3434
): this {
35-
const localizableStrings: I18nEntry[] = [];
36-
const infoPlistStrings: I18nEntry[] = [];
37-
for (const { key, value } of i18nContentIterator) {
38-
localizableStrings.push({ key, value });
35+
const infoPlistStrings: I18nEntries = new Map();
36+
i18nEntries.forEach((value, key) => {
3937
if (key === "app.name") {
40-
infoPlistStrings.push({ key: "CFBundleDisplayName", value });
41-
infoPlistStrings.push({ key: "CFBundleName", value });
38+
infoPlistStrings.set("CFBundleDisplayName", value);
39+
infoPlistStrings.set("CFBundleName", value);
4240
} else if (key.startsWith("ios.info.plist.")) {
43-
infoPlistStrings.push({ key: key.substr(15), value });
41+
infoPlistStrings.set(key.substr(15), value);
4442
}
45-
}
43+
});
4644
const languageResourcesDir = path.join(this.appResourcesDirectoryPath, `${language}.lproj`);
4745
this
4846
.createDirectoryIfNeeded(languageResourcesDir)
49-
.writeStrings(languageResourcesDir, "Localizable.strings", localizableStrings, true)
47+
.writeStrings(languageResourcesDir, "Localizable.strings", i18nEntries, true)
5048
.writeStrings(languageResourcesDir, "InfoPlist.strings", infoPlistStrings, false)
5149
;
5250
if (isDefaultLanguage) {
53-
infoPlistStrings.push({ key: "CFBundleDevelopmentRegion", value: language });
51+
infoPlistStrings.set("CFBundleDevelopmentRegion", language);
5452
this.writeInfoPlist(infoPlistStrings);
5553
}
5654
return this;
@@ -66,34 +64,34 @@ export class ConverterIOS extends ConverterCommon {
6664
private writeStrings(
6765
languageResourcesDir: string,
6866
resourceFileName: string,
69-
strings: I18nEntry[],
67+
strings: I18nEntries,
7068
encodeKeys: boolean
7169
): this {
7270
let content = "";
73-
for (const { key, value } of strings) {
71+
strings.forEach((value, key) => {
7472
content += `"${encodeKeys ? encodeKey(key) : key}" = "${encodeValue(value)}";\n`;
75-
}
73+
});
7674
const resourceFilePath = path.join(languageResourcesDir, resourceFileName);
7775
if (this.writeFileSyncIfNeeded(resourceFilePath, content)) {
7876
this.emit(ConverterCommon.RESOURCE_CHANGED_EVENT);
7977
}
8078
return this;
8179
}
8280

83-
private writeInfoPlist(infoPlistValues: I18nEntry[]) {
81+
private writeInfoPlist(infoPlistValues: I18nEntries) {
8482
const resourceFilePath = path.join(this.appResourcesDirectoryPath, "Info.plist");
8583
if (!fs.existsSync(resourceFilePath)) {
8684
this.logger.warn(`'${resourceFilePath}' doesn't exists: unable to set default language`);
8785
return this;
8886
}
8987
const data = plist.readFileSync(resourceFilePath);
9088
let resourceChanged = false;
91-
for (const { key, value } of infoPlistValues) {
89+
infoPlistValues.forEach((value, key) => {
9290
if (!data.hasOwnProperty(key) || data[key] !== value) {
9391
data[key] = value;
9492
resourceChanged = true;
9593
}
96-
}
94+
});
9795
if (resourceChanged) {
9896
plist.writeFileSync(resourceFilePath, data);
9997
this.emit(ConverterCommon.CONFIGURATION_CHANGED_EVENT);

0 commit comments

Comments
 (0)