Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated translations, added build tool to generate translations for any language. #320

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions build/translation-generator/generate-translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
declare namespace Intl {

type RelativeTimeUnit = "year" | "quarter" | "month" | "week" | "day" | "hour" | "minute" | "second" |
"years" | "quarters" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds";

interface LocaleMatchable {
localeMatcher?: "best fit" | "lookup";
}
interface FormatPart {
type: "integer" | "string";
value: number | string;
}
interface IntegerFormatPart extends FormatPart {
type: "integer";
value: number;
unit: string;
}
interface LiteralFormatPart {
type: "literal";
value: string;
}
interface RelativeTimeOptions extends LocaleMatchable {
numeric?: "auto" | "always",
style?: "long" | "short" | "narrow";
}

interface RelativeTimeFormat {
format: (value: number, unit: RelativeTimeUnit) => string;
formatToParts: (value: number, unit: RelativeTimeUnit) => Array<IntegerFormatPart>;
resolvedOptions(): RelativeTimeOptions;
}

var RelativeTimeFormat: {
new(locales?: string | string[], options?: RelativeTimeOptions): RelativeTimeFormat;
(locales?: string | string[], options?: RelativeTimeOptions): RelativeTimeFormat;
supportedLocalesOf(locales: string | string[], options?: LocaleMatchable): string[];
};
}

export type TranslationData = Record<string, { translation: Translation; }>;

export interface TranslationTemplate<T> extends Record<string, T> {
second_ago: T;
second_ago_plural: T;
second_in: T;
second_in_plural: T;
minute_ago: T;
minute_ago_plural: T;
minute_in: T;
minute_in_plural: T;
hour_ago: T;
hour_ago_plural: T;
hour_in: T;
hour_in_plural: T;
day_ago: T;
day_ago_plural: T;
day_in: T;
day_in_plural: T;
month_ago: T;
month_ago_plural: T;
month_in: T;
month_in_plural: T;
year_ago: T;
year_ago_plural: T;
year_in: T;
year_in_plural: T;
}

export type Translation = TranslationTemplate<string>;

const template: TranslationTemplate<[number, Intl.RelativeTimeUnit]> = {
now: [0, "second"],
second_ago: [-1, "second"],
second_ago_plural: [-53, "second"],
second_in: [1, "second"],
second_in_plural: [53, "second"],
minute_ago: [-1, "minute"],
minute_ago_plural: [-53, "minute"],
minute_in: [1, "minute"],
minute_in_plural: [53, "minute"],
hour_ago: [-1, "hour"],
hour_ago_plural: [-53, "hour"],
hour_in: [1, "hour"],
hour_in_plural: [53, "hour"],
day_ago: [-1, "day"],
day_ago_plural: [-53, "day"],
day_in: [1, "day"],
day_in_plural: [53, "day"],
month_ago: [-1, "month"],
month_ago_plural: [-53, "month"],
month_in: [1, "month"],
month_in_plural: [53, "month"],
year_ago: [-1, "year"],
year_ago_plural: [-53, "year"],
year_in: [1, "year"],
year_in_plural: [53, "year"],
};

function getRuntimeSupportedLocales(list: string[]): string[] {
const result = [];
let batchIndex = 0;
const batchSize = 10;
while (batchIndex + batchSize < list.length) {
const batch = list.slice(
batchIndex,
Math.min(batchIndex + batchSize, list.length)
);
const supported = Intl.RelativeTimeFormat.supportedLocalesOf(batch);
result.push(...supported);
batchIndex += batchSize;
}
return result;
}

function buildCompleteTranslations(locales: string[], interpolationPrefix: string, interpolationSuffix: string): TranslationData {

const allTranslations: TranslationData = {};

for (const locale of locales) {
const translation: Partial<Translation> = {};

const numericRelTime = new Intl.RelativeTimeFormat(locale, { numeric: "always", style: "long" });
const autoNumericRelTime = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "long" });

Object.keys(template).forEach(key => {
const params = template[key];
const rt = params[0] === 0 ? autoNumericRelTime : numericRelTime;
const parts = rt.formatToParts(...params);

const val = parts.map(part => {
if (!["literal", "integer"].includes(part.type)) {
throw new Error(`Unexpected part! locale ${locale}, key: ${key}, type: ${part.type}`);
}
return part.type === "integer" ? `${interpolationPrefix}count${interpolationSuffix}` : part.value;
}).join("");

translation[key] = val;
});

allTranslations[locale] = { translation: translation as Translation };
}

return allTranslations;

}

function areEqual(tr1: Translation, tr2: Translation): boolean {
const tr1Keys = Object.keys(tr1).sort();
const tr2Keys = Object.keys(tr2).sort();
const keysAreEqual = tr1Keys.join("") === tr2Keys.join("");
if (!keysAreEqual) {
return false;
}
let result = true;
for (const key of tr1Keys) {
if (tr1[key] !== tr2[key]) {
result = false;
break;
}
}
return result;
}

function removeRedundantSubLocaleTranslations(trData: TranslationData): string[] {
const subLocales = Object.keys(trData).filter(k => k.includes("-"));
const redundantSubLocales: string[] = [];
for (const sl of subLocales) {
const mainLocale = sl.split("-")[0];
if (trData[mainLocale] && areEqual(trData[mainLocale].translation, trData[sl].translation)) {
redundantSubLocales.push(sl);
delete trData[sl];
}
}
return redundantSubLocales;
}

export function generateTranslations(
locales: string[],
interpolationPrefix: string = "__",
interpolationSuffix: string = "__"
): TranslationData {

const runtimeSupportedLocales = getRuntimeSupportedLocales(locales);

// Getting a list of the unsupported locales for debugging
const runtimeNonSupportedLocales = locales.filter(l => !runtimeSupportedLocales.includes(l));
console.log("[translationgen] Ignoring following runtime unsupported locales:", runtimeNonSupportedLocales);

console.log("[translationgen] Generating translations for the following runtime supported locales:", runtimeSupportedLocales);
const translations = buildCompleteTranslations(runtimeSupportedLocales, interpolationPrefix, interpolationSuffix);

const redundant = removeRedundantSubLocaleTranslations(translations);
console.log("[translationgen] Removing the following redundant (identical) sub-locale translations:", redundant);

return translations;
}
46 changes: 46 additions & 0 deletions build/translation-generator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as fs from "fs";
import * as path from "path";
import * as process from "process";
import { generateTranslations, TranslationData } from "./generate-translations";

/**
* Generates translations file by using Intl.RelativeTimeFormat with node full-icu
* Can generate the full translation for any language, except the "now" variable
* command line args: outputPath for file to generate
*/

const outFile = process.argv[2];
if (!outFile || !fs.existsSync(path.dirname(outFile))) {
throw new Error("Invalid or missing output file argument");
}

function toTsCode(data: TranslationData): string {
const transJs = JSON.stringify(data, null, 2)
.replace(/"([^"-]+)":/g, (_, g) => `${g}:`)
.replace(/"/g, "'");

return `// tslint:disable
export type DefaultTranslations = {
[key: string]: string | DefaultTranslations
}

export const translations: DefaultTranslations = ${transJs};
// tslint:enable
`;
}

// Get list of locales to generate translations for.
const locales = require("./supported-locales");

// Generate translations for specified list of locales, use "__" as count interpolation prefix/suffix:
const translations = generateTranslations(locales, "__", "__");

// Convert to TS file similar to "src/defaultTranslations/relative.time.ts":
const content = toTsCode(translations);

// Alternatively convert to JSON
//const content = JSON.stringify(translations, null, 2);

console.log("[translationgen] Writing generated output file...");
fs.writeFileSync(outFile, content);
console.log("[translationgen] Done!");
6 changes: 6 additions & 0 deletions build/translation-generator/supported-locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// List of the locales supported in aurelia-i18n
const locales: string[] = [
"ar", "da", "de", "en", "es", "fi", "fr", "it", "ja", "lt", "nl", "nn", "nb", "pl", "pt", "sv", "th", "zh", "zh-CN", "zh-HK", "zh-Hans-HK"
];

export = locales;
Loading