diff --git a/app/client/components/TypeConversion.ts b/app/client/components/TypeConversion.ts index 0fbfd399..cb97fa32 100644 --- a/app/client/components/TypeConversion.ts +++ b/app/client/components/TypeConversion.ts @@ -7,6 +7,7 @@ import {isString} from 'app/client/lib/sessionObs'; import {DocModel} from 'app/client/models/DocModel'; import {ColumnRec} from 'app/client/models/entities/ColumnRec'; +import {csvDecodeRow} from 'app/common/csvFormat'; import * as gristTypes from 'app/common/gristTypes'; import {isFullReferencingType} from 'app/common/gristTypes'; import * as gutil from 'app/common/gutil'; @@ -175,7 +176,7 @@ export async function prepTransformColInfo(docModel: DocModel, origCol: ColumnRe for (let value of tableData.getColValues(sourceCol.colId()) || []) { if (value === null) { continue; } value = String(decodeObject(value)).trim(); - const tags: unknown[] = (value.startsWith('[') && gutil.safeJsonParse(value, null)) || value.split(","); + const tags: unknown[] = (value.startsWith('[') && gutil.safeJsonParse(value, null)) || csvDecodeRow(value); for (const tag of tags) { choices.add(String(tag).trim()); if (choices.size > 100) { break; } // Don't suggest excessively many choices. diff --git a/buildtools/generate_translation_keys.js b/buildtools/generate_translation_keys.js index 81cad4bf..1c013c54 100644 --- a/buildtools/generate_translation_keys.js +++ b/buildtools/generate_translation_keys.js @@ -65,20 +65,66 @@ const getKeysFromFile = (filePath, fileName) => { return keys; }; +// It is highly desirable to retain existing order, to not generate +// unnecessary merges/conflicts, so we do a specialized merge. +function merge(target, scanned) { + let merges = 0; + for (const key of Object.keys(scanned)) { + if (!(key in target)) { + console.log("Merging key", {key}); + target[key] = scanned[key]; + merges++; + } else if (typeof target[key] === 'object') { + merges += merge(target[key], scanned[key]); + } else if (scanned[key] !== target[key]) { + if (!key.endsWith('_one')) { + console.log("Value difference", {key, value: target[key]}); + } + } + } + return merges; +} + +// Look for keys that are listed in json file but not found in source +// code. These may be stale and need deleting in weblate. +function reportUnrecognizedKeys(originalKeys, foundKeys) { + let unknowns = 0; + for (const section of Object.keys(originalKeys)) { + if (!(section in foundKeys)) { + console.log("Unknown section found", {section}); + unknowns++; + } else { + for (const key of Object.keys(originalKeys[section])) { + if (!(key in foundKeys[section])) { + console.log("Unknown key found", {section, key}); + unknowns++; + } + } + } + } + return unknowns; +} + async function walkTranslation(dirs) { + const originalKeys = _.cloneDeep(englishKeys); for await (const p of walk(dirs)) { const { name } = path.parse(p); if (p.endsWith('.map')) { continue; } getKeysFromFile(p, name); } const keys = parser.get({ sort: true }); - const newTranslations = _.merge(keys.en.translation, englishKeys); + const foundKeys = _.cloneDeep(keys.en.translation); + const mergeCount = merge(englishKeys, sort(keys.en.translation)); await fs.promises.writeFile( "static/locales/en.client.json", - JSON.stringify(sort(newTranslations), null, 2), + JSON.stringify(englishKeys, null, 4) + '\n', // match weblate's default "utf-8" ); - return keys; + // Now, print a report of unrecognized keys - candidates + // for deletion in weblate. + const unknownCount = reportUnrecognizedKeys(originalKeys, foundKeys); + console.log(`Found ${unknownCount} unknown key(s).`); + console.log(`Make ${mergeCount} merge(s).`); } walkTranslation(["_build/app/client", ...process.argv.slice(2)]); diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 4f97b652..c7e8bf20 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -36,7 +36,9 @@ "Special Rules": "Special Rules", "Type a message...": "Type a message...", "User Attributes": "User Attributes", - "View As": "View As" + "View As": "View As", + "Seed rules": "Seed rules", + "When adding table rules, automatically add a rule to grant OWNER full access.": "When adding table rules, automatically add a rule to grant OWNER full access." }, "AccountPage": { "API": "API", @@ -332,7 +334,8 @@ "Add Column": "Add Column" }, "FilterBar": { - "SearchColumns": "Search columns" + "SearchColumns": "Search columns", + "Search Columns": "Search Columns" }, "GridOptions": { "Grid Options": "Grid Options", @@ -761,5 +764,17 @@ "NTextBox": { "false": "false", "true": "true" + }, + "ACLUsers": { + "Example Users": "Example Users", + "Users from table": "Users from table", + "View As": "View As" + }, + "TypeTransform": { + "Apply": "Apply", + "Cancel": "Cancel", + "Preview": "Preview", + "Revise": "Revise", + "Update formula (Shift+Enter)": "Update formula (Shift+Enter)" } } diff --git a/test/nbrowser/ChoiceList.ts b/test/nbrowser/ChoiceList.ts index d528ecb9..901bcec5 100644 --- a/test/nbrowser/ChoiceList.ts +++ b/test/nbrowser/ChoiceList.ts @@ -447,7 +447,7 @@ describe('ChoiceList', function() { it('should allow reasonable conversions between ChoiceList and other types', async function() { await gu.enterGridRows({rowNum: 1, col: 'A'}, - [['Hello'], ['World'], [' Foo,Bar;Baz!,']]); + [['Hello'], ['World'], [' Foo,Bar;Baz!,"Qux, quux corge", "80\'s",']]); await testTextChoiceListConversions(); }); @@ -468,17 +468,17 @@ describe('ChoiceList', function() { // Check that choices got populated. await driver.find('.test-right-tab-field').click(); - assert.deepEqual(await getChoiceLabels(), ['Hello', 'World', 'Foo', 'Bar;Baz!']); + assert.deepEqual(await getChoiceLabels(), ['Hello', 'World', 'Foo', 'Bar;Baz!', 'Qux, quux corge', '80\'s']); assert.deepEqual( await getChoiceColors(), - [UNSET_FILL, UNSET_FILL, UNSET_FILL, UNSET_FILL] + [UNSET_FILL, UNSET_FILL, UNSET_FILL, UNSET_FILL, UNSET_FILL, UNSET_FILL] ); // Check that the result contains the right tags. assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['A']}), [ 'Hello', 'World', - 'Foo\nBar;Baz!' + 'Foo\nBar;Baz!\nQux, quux corge\n80\'s' ]); await gu.checkForErrors(); @@ -494,17 +494,21 @@ describe('ChoiceList', function() { [ {fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}, {fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}, + {fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}, + {fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}, ] ); // Open a cell to see the actual tags. await gu.getCell({rowNum: 3, col: 'A'}).click(); await driver.sendKeys(Key.ENTER); - assert.deepEqual(await getEditorTokens(), ['Foo', 'Bar;Baz!']); - assert.deepEqual(await getEditorTokensIsInvalid(), [false, false]); + assert.deepEqual(await getEditorTokens(), ['Foo', 'Bar;Baz!', 'Qux, quux corge', '80\'s']); + assert.deepEqual(await getEditorTokensIsInvalid(), [ false, false, false, false ]); assert.deepEqual( await getEditorTokenStyles(), [ + {fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}, + {fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}, {fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}, {fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE} ] @@ -520,7 +524,7 @@ describe('ChoiceList', function() { assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['A']}), [ 'Hello', 'World', - 'Foo, Bar;Baz!, hooray' + 'Foo, Bar;Baz!, "Qux, quux corge", 80\'s, hooray' ]); // Undo the cell change and both conversions (back to ChoiceList, back to Text), and check @@ -529,7 +533,7 @@ describe('ChoiceList', function() { assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['A']}), [ 'Hello', 'World', - ' Foo,Bar;Baz!,', // That's the text originally entered into this Text cell. + ' Foo,Bar;Baz!,"Qux, quux corge", "80\'s",', // That's the text originally entered into this Text cell. ]); }