mirror of
https://github.com/wassname/talk.git
synced 2026-08-20 12:50:41 +08:00
[next] i18n short number support (#1992)
* feat: Implement i18n short number * test: fix failing tests
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { FluentNumber, FluentType } from "fluent/compat";
|
||||
|
||||
import { FluentShortNumber } from "../types";
|
||||
|
||||
export default function SHORT_NUMBER([t]: [FluentType]) {
|
||||
if (!(t instanceof FluentNumber)) {
|
||||
throw new Error(`Invalid argument for SHORT_NUMBER ${t.valueOf()}`);
|
||||
}
|
||||
return new FluentShortNumber(t.valueOf());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SHORT_NUMBER } from "./SHORT_NUMBER";
|
||||
+4
-46
@@ -1,50 +1,8 @@
|
||||
import "fluent-intl-polyfill/compat";
|
||||
import { negotiateLanguages as negotiate } from "fluent-langneg/compat";
|
||||
import { FluentBundle } from "fluent/compat";
|
||||
|
||||
export interface BundledLocales {
|
||||
[locale: string]: string;
|
||||
}
|
||||
|
||||
export interface LoadableLocales {
|
||||
[locale: string]: (() => Promise<string>);
|
||||
}
|
||||
|
||||
/**
|
||||
* This type describes the shape of the generated code from our `locales-loader`.
|
||||
* Please check `./src/loaders` and the webpack config for more information.
|
||||
*/
|
||||
export interface LocalesData {
|
||||
readonly defaultLocale: string;
|
||||
readonly fallbackLocale: string;
|
||||
readonly availableLocales: ReadonlyArray<string>;
|
||||
readonly bundled: BundledLocales;
|
||||
readonly loadables: LoadableLocales;
|
||||
}
|
||||
|
||||
/**
|
||||
* negotiateLanguages accepts `userLocales` which usually comes from
|
||||
* `navigator.languages` and the locales `data` as generated by
|
||||
* the `locales-loader` and returns an array of matching languages.
|
||||
*/
|
||||
export function negotiateLanguages(
|
||||
userLocales: ReadonlyArray<string>,
|
||||
data: LocalesData
|
||||
) {
|
||||
// Choose locale that is best for the user.
|
||||
const languages = negotiate(userLocales, data.availableLocales, {
|
||||
defaultLocale: data.defaultLocale,
|
||||
strategy: "lookup",
|
||||
});
|
||||
|
||||
if (data.fallbackLocale && languages[0] !== data.fallbackLocale) {
|
||||
// Use default locale as fallback in case we have
|
||||
// missing keys.
|
||||
languages.push(data.fallbackLocale);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
import * as functions from "./functions";
|
||||
import { LocalesData } from "./locales";
|
||||
|
||||
// Don't warn in production.
|
||||
let decorateWarnMissing = (bundle: FluentBundle) => bundle;
|
||||
@@ -81,14 +39,14 @@ if (process.env.NODE_ENV !== "production") {
|
||||
*
|
||||
* Use it in conjunction with `negotiateLanguages`.
|
||||
*/
|
||||
export async function generateBundles(
|
||||
export default async function generateBundles(
|
||||
locales: ReadonlyArray<string>,
|
||||
data: LocalesData
|
||||
): Promise<FluentBundle[]> {
|
||||
const promises = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
const bundle = new FluentBundle(locale);
|
||||
const bundle = new FluentBundle(locale, { functions });
|
||||
if (locale in data.bundled) {
|
||||
bundle.addMessages(data.bundled[locale]);
|
||||
promises.push(decorateWarnMissing(bundle));
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as generateBundles } from "./generateBundles";
|
||||
export { default as negotiateLanguages } from "./negotiateLanguages";
|
||||
export { BundledLocales, LoadableLocales, LocalesData } from "./locales";
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface BundledLocales {
|
||||
[locale: string]: string;
|
||||
}
|
||||
|
||||
export interface LoadableLocales {
|
||||
[locale: string]: (() => Promise<string>);
|
||||
}
|
||||
|
||||
/**
|
||||
* This type describes the shape of the generated code from our `locales-loader`.
|
||||
* Please check `./src/loaders` and the webpack config for more information.
|
||||
*/
|
||||
export interface LocalesData {
|
||||
readonly defaultLocale: string;
|
||||
readonly fallbackLocale: string;
|
||||
readonly availableLocales: ReadonlyArray<string>;
|
||||
readonly bundled: BundledLocales;
|
||||
readonly loadables: LoadableLocales;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { negotiateLanguages as negotiate } from "fluent-langneg/compat";
|
||||
|
||||
import { LocalesData } from "./locales";
|
||||
|
||||
/**
|
||||
* negotiateLanguages accepts `userLocales` which usually comes from
|
||||
* `navigator.languages` and the locales `data` as generated by
|
||||
* the `locales-loader` and returns an array of matching languages.
|
||||
*/
|
||||
export default function negotiateLanguages(
|
||||
userLocales: ReadonlyArray<string>,
|
||||
data: LocalesData
|
||||
) {
|
||||
// Choose locale that is best for the user.
|
||||
const languages = negotiate(userLocales, data.availableLocales, {
|
||||
defaultLocale: data.defaultLocale,
|
||||
strategy: "lookup",
|
||||
});
|
||||
|
||||
if (data.fallbackLocale && languages[0] !== data.fallbackLocale) {
|
||||
// Use default locale as fallback in case we have
|
||||
// missing keys.
|
||||
languages.push(data.fallbackLocale);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { toPairs } from "lodash";
|
||||
import { getShortNumberCode, validateFormat } from "./FluentShortNumber";
|
||||
|
||||
describe("getShortNumberCode", () => {
|
||||
it("returns correct value", () => {
|
||||
const cases = {
|
||||
123: "100",
|
||||
4322: "1000",
|
||||
33223: "10000",
|
||||
};
|
||||
toPairs(cases).forEach(([i, o]) => {
|
||||
expect(getShortNumberCode(parseFloat(i))).toBe(o);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateFormat", () => {
|
||||
it("returns correct value", () => {
|
||||
const cases = {
|
||||
"0k": true,
|
||||
"0kilo": true,
|
||||
"0.0": false,
|
||||
"0": false,
|
||||
"0.": false,
|
||||
"0.0k": true,
|
||||
"000.0k": true,
|
||||
"000M": true,
|
||||
};
|
||||
toPairs(cases).forEach(([i, o]) => {
|
||||
expect(validateFormat(i)).toBe(o);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { FluentBundle, FluentNumber, FluentType } from "fluent/compat";
|
||||
|
||||
const formatRegExp = /^(0+|0+\.0+)[^\d\.]+$/;
|
||||
|
||||
export function validateFormat(fmt: string) {
|
||||
return formatRegExp.test(fmt);
|
||||
}
|
||||
|
||||
export function getShortNumberCode(n: number) {
|
||||
let code = "1";
|
||||
while (n >= 10) {
|
||||
n /= 10;
|
||||
code += "0";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function formatShortNumber(n: number, format: string, bundle: FluentBundle) {
|
||||
const lastIndexOf0 = format.lastIndexOf("0");
|
||||
const unit = format.substr(lastIndexOf0 + 1);
|
||||
const rest = format.substr(0, lastIndexOf0 + 1);
|
||||
const splitted = rest.split(".");
|
||||
const digits = splitted[0].length;
|
||||
const fractalDigits = (splitted.length > 1 && splitted[1].length) || 0;
|
||||
const threshold = Math.pow(10, digits);
|
||||
while (n > threshold) {
|
||||
n /= 10;
|
||||
}
|
||||
const formattedNumber = new FluentNumber(n, {
|
||||
maximumFractionDigits: fractalDigits,
|
||||
}).toString(bundle);
|
||||
return `${formattedNumber}${unit}`;
|
||||
}
|
||||
|
||||
export default class FluentShortNumber extends FluentNumber {
|
||||
constructor(value: any, opts?: any) {
|
||||
super(value, opts);
|
||||
}
|
||||
|
||||
public toString(bundle: FluentBundle) {
|
||||
if (this.value < 1000) {
|
||||
return super.toString(bundle);
|
||||
}
|
||||
const key = `framework-shortNumber-${getShortNumberCode(this.value)}`;
|
||||
const fmt = bundle.getMessage(key);
|
||||
|
||||
// Handle message not found.
|
||||
if (!fmt) {
|
||||
const message = `Missing translation key for ${key} for languages ${bundle.locales.toString()}`;
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(message);
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
return super.toString(bundle);
|
||||
}
|
||||
|
||||
// Check for invalid message.
|
||||
if (!validateFormat(fmt)) {
|
||||
const message = `Invalid Short Number Format ${fmt}`;
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(message);
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
return super.toString(bundle);
|
||||
}
|
||||
|
||||
return formatShortNumber(this.value, fmt, bundle);
|
||||
}
|
||||
|
||||
public match(bundle: FluentBundle, other: FluentType) {
|
||||
if (other instanceof FluentShortNumber) {
|
||||
return this.value === other.valueOf;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FluentShortNumber } from "./FluentShortNumber";
|
||||
@@ -1,4 +1,8 @@
|
||||
import "fluent-intl-polyfill/compat";
|
||||
import { FluentBundle } from "fluent/compat";
|
||||
|
||||
import * as functions from "talk-framework/lib/i18n/functions";
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
@@ -34,7 +38,7 @@ function createFluentBundle(
|
||||
target: string,
|
||||
pathToLocale: string
|
||||
): FluentBundle {
|
||||
const bundle = new FluentBundle("en-US");
|
||||
const bundle = new FluentBundle("en-US", { functions });
|
||||
const files = fs.readdirSync(pathToLocale);
|
||||
const prefixes = commonPrefixes.concat(target);
|
||||
files.forEach(f => {
|
||||
|
||||
Reference in New Issue
Block a user