(core) Simple localization support and currency selector.

Summary:
- Grist document has a associated "locale" setting that affects how currency is formatted.
- Currency selector for number format.

Test Plan: not done

Reviewers: dsagal

Reviewed By: dsagal

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D2977
This commit is contained in:
George Gevoian
2021-08-26 09:35:11 -07:00
parent e492dfdb22
commit a6e08883e0
36 changed files with 405 additions and 84 deletions

View File

@@ -85,6 +85,7 @@ bluebird.promisifyAll(tmp);
const MAX_RECENT_ACTIONS = 100;
const DEFAULT_TIMEZONE = (process.versions as any).electron ? moment.tz.guess() : "UTC";
const DEFAULT_LOCALE = "en-US";
// Number of seconds an ActiveDoc is retained without any clients.
// In dev environment, it is convenient to keep this low for quick tests.
@@ -338,10 +339,11 @@ export class ActiveDoc extends EventEmitter {
await this._docManager.storageManager.prepareToCreateDoc(this.docName);
await this.docStorage.createFile();
await this._rawPyCall('load_empty');
const timezone = docSession.browserSettings ? docSession.browserSettings.timezone : DEFAULT_TIMEZONE;
const timezone = docSession.browserSettings?.timezone ?? DEFAULT_TIMEZONE;
const locale = docSession.browserSettings?.locale ?? DEFAULT_LOCALE;
// This init action is special. It creates schema tables, and is used to init the DB, but does
// not go through other steps of a regular action (no ActionHistory or broadcasting).
const initBundle = await this._rawPyCall('apply_user_actions', [["InitNewDoc", timezone]]);
const initBundle = await this._rawPyCall('apply_user_actions', [["InitNewDoc", timezone, locale]]);
await this.docStorage.execTransaction(() =>
this.docStorage.applyStoredActions(getEnvContent(initBundle.stored)));

View File

@@ -84,7 +84,8 @@ export class Client {
constructor(
private _comm: any,
private _methods: any,
private _host: string
private _host: string,
private _locale?: string,
) {
this.clientId = generateClientId();
}
@@ -102,6 +103,10 @@ export class Client {
return this._host;
}
public get locale(): string|undefined {
return this._locale;
}
public setConnection(websocket: any, reqHost: string, browserSettings: BrowserSettings) {
this._websocket = websocket;
// Set this._loginState, used by CognitoClient to construct login/logout URLs.

View File

@@ -49,6 +49,7 @@ var gutil = require('app/common/gutil');
const {parseFirstUrlPart} = require('app/common/gristUrls');
const version = require('app/common/version');
const {Client} = require('./Client');
const {localeFromRequest} = require('app/server/lib/ServerLocale');
// Bluebird promisification, to be able to use e.g. websocket.sendAsync method.
Promise.promisifyAll(ws.prototype);
@@ -216,7 +217,7 @@ Comm.prototype._onWebSocketConnection = async function(websocket, req) {
}
client.setConnection(websocket, req.headers.host, browserSettings);
} else {
client = new Client(this, this.methods, req.headers.host);
client = new Client(this, this.methods, req.headers.host, localeFromRequest(req));
client.setCounter(counter);
client.setConnection(websocket, req.headers.host, browserSettings);
this._clients[client.clientId] = client;

View File

@@ -32,6 +32,7 @@ import { exportToDrive } from "app/server/lib/GoogleExport";
import { googleAuthTokenMiddleware } from "app/server/lib/GoogleAuth";
import * as _ from "lodash";
import {isRaisedException} from "app/common/gristTypes";
import {localeFromRequest} from "app/server/lib/ServerLocale";
// Cap on the number of requests that can be outstanding on a single document via the
// rest doc api. When this limit is exceeded, incoming requests receive an immediate
@@ -590,6 +591,7 @@ export class DocWorkerApi {
if (parameters.workspaceId) { throw new Error('workspaceId not supported'); }
const browserSettings: BrowserSettings = {};
if (parameters.timezone) { browserSettings.timezone = parameters.timezone; }
browserSettings.locale = localeFromRequest(req);
if (uploadId !== undefined) {
const result = await this._docManager.importDocToWorkspace(userId, uploadId, null,
browserSettings);

View File

@@ -24,6 +24,7 @@ export interface OptDocSession {
export function makeOptDocSession(client: Client|null, browserSettings?: BrowserSettings): OptDocSession {
if (client && !browserSettings) { browserSettings = client.browserSettings; }
if (client && browserSettings && !browserSettings.locale) { browserSettings.locale = client.locale; }
return {client, browserSettings};
}

View File

@@ -3,7 +3,7 @@ import {GristType} from 'app/common/gristTypes';
import * as gutil from 'app/common/gutil';
import * as gristTypes from 'app/common/gristTypes';
import {NumberFormatOptions} from 'app/common/NumberFormat';
import {formatUnknown, IsRightTypeFunc} from 'app/common/ValueFormatter';
import {FormatOptions, formatUnknown, IsRightTypeFunc} from 'app/common/ValueFormatter';
import {decodeObject} from 'app/plugin/objtypes';
import {Style} from 'exceljs';
import * as moment from 'moment-timezone';
@@ -17,9 +17,9 @@ interface WidgetOptions extends NumberFormatOptions {
}
class BaseFormatter {
protected isRightType: IsRightTypeFunc;
protected widgetOptions: WidgetOptions = {};
protected widgetOptions: WidgetOptions;
constructor(public type: string, public opts: object) {
constructor(public type: string, public opts: FormatOptions) {
this.isRightType = gristTypes.isRightType(gristTypes.extractTypeFromColType(type)) ||
gristTypes.isRightType('Any')!;
this.widgetOptions = opts;
@@ -164,11 +164,11 @@ const formatters: Partial<Record<GristType, typeof BaseFormatter>> = {
};
/**
* Takes column type and widget options and returns a constructor with a format function that can
* Takes column type and format options and returns a constructor with a format function that can
* properly convert a value passed to it into the right javascript object for that column.
* Exceljs library is using javascript primitives to specify correct excel type.
*/
export function createExcelFormatter(type: string, opts: object): BaseFormatter {
export function createExcelFormatter(type: string, opts: FormatOptions): BaseFormatter {
const ctor = formatters[gristTypes.extractTypeFromColType(type) as GristType] || AnyFormatter;
return new ctor(type, opts);
}

View File

@@ -7,6 +7,7 @@ import {buildRowFilter} from 'app/common/RowFilterFunc';
import {SchemaTypes} from 'app/common/schema';
import {SortFunc} from 'app/common/SortFunc';
import {TableData} from 'app/common/TableData';
import {DocumentSettings} from 'app/common/DocumentSettings';
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {RequestWithLogin} from 'app/server/lib/Authorizer';
import {docSessionFromRequest} from 'app/server/lib/DocSession';
@@ -60,6 +61,10 @@ export interface ExportData {
* Columns information (primary used for formatting).
*/
columns: ExportColumn[];
/**
* Document settings
*/
docSettings: DocumentSettings;
}
/**
@@ -194,12 +199,15 @@ export async function exportTable(
tableName = view.name;
}
const docInfo = safeRecord(safeTable(docData, '_grist_DocInfo'), 1) as DocInfo;
const docSettings = gutil.safeJsonParse(docInfo.documentSettings, {});
return {
tableName,
docName: activeDoc.docName,
rowIds,
access,
columns
columns,
docSettings
};
}
@@ -277,11 +285,15 @@ export async function exportSection(
// filter rows numbers
rowIds = rowIds.filter(rowFilter);
const docInfo = safeRecord(safeTable(docData, '_grist_DocInfo'), 1) as DocInfo;
const docSettings = gutil.safeJsonParse(docInfo.documentSettings, {});
return {
tableName: table.tableId,
docName: activeDoc.docName,
rowIds,
access,
docSettings,
columns: viewColumns
};
}
@@ -295,6 +307,7 @@ type GristTables = RowModel<'_grist_Tables'>
type GristViewsSectionField = RowModel<'_grist_Views_section_field'>
type GristTablesColumn = RowModel<'_grist_Tables_column'>
type GristView = RowModel<'_grist_Views'>
type DocInfo = RowModel<'_grist_DocInfo'>
// Type for filters passed from the client
export interface Filter { colRef: number, filter: string }

View File

@@ -33,11 +33,12 @@ export async function makeCSV(
function convertToCsv({
rowIds,
access,
columns: viewColumns
columns: viewColumns,
docSettings
}: ExportData) {
// create formatters for columns
const formatters = viewColumns.map(col => createFormatter(col.type, col.widgetOptions));
const formatters = viewColumns.map(col => createFormatter(col.type, col.widgetOptions, docSettings));
// Arrange the data into a row-indexed matrix, starting with column headers.
const csvMatrix = [viewColumns.map(col => col.label)];
// populate all the rows with values as strings

View File

@@ -0,0 +1,16 @@
import {parse as languageParser} from "accept-language-parser";
import {Request} from 'express';
import {locales} from 'app/common/Locales';
/**
* Returns the locale from a request, falling back to `defaultLocale`
* if unable to determine the locale.
*/
export function localeFromRequest(req: Request, defaultLocale: string = 'en-US') {
const language = languageParser(req.headers["accept-language"] as string)[0];
if (!language) { return defaultLocale; }
const locale = `${language.code}-${language.region}`;
const supports = locales.some(l => l.code === locale);
return supports ? locale : defaultLocale;
}

View File

@@ -1,7 +1,7 @@
{
"extends": "../../buildtools/tsconfig-base.json",
"references": [
{ "path": "../common" }
{ "path": "../common" },
],
"include": [
"**/*",