Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
23e85b0
Adds .txt for i18n files
zachmargolis Apr 24, 2024
430efb3
Try migrating fixtures for rails-i18n-webpack-plugin
zachmargolis Apr 25, 2024
19c7e6c
WIP
zachmargolis Apr 25, 2024
1af7b08
Remove domain concept from rails-i18n-webpack-plugin
aduth Apr 25, 2024
b0f4983
Remove unused helper functions
aduth Apr 25, 2024
e12d255
Rename to "FlatYml" backend, since we can parse as YML
zachmargolis Apr 25, 2024
85b67cb
finish that last commit
zachmargolis Apr 25, 2024
9894631
Split transliteration back out
zachmargolis Apr 25, 2024
7a8ec9a
Update normalize-yaml
zachmargolis Apr 25, 2024
1e9cd18
Bring back separate telephony/*.yml files
zachmargolis Apr 25, 2024
927591f
Changelog notes for normalize-yaml
zachmargolis Apr 25, 2024
be66fe6
Empty check in script
zachmargolis May 1, 2024
ea65e62
Merge remote-tracking branch 'origin/main' into margolis-i18n-txt-bac…
zachmargolis May 1, 2024
d34674e
Add scripts/fix_yml_merge_conflicts to fix addressing issues with lon…
zachmargolis May 1, 2024
16705f1
True up en.yml, fix script
zachmargolis May 1, 2024
ced01a7
Update i18n-tasks
zachmargolis May 1, 2024
1dd84e3
wi
zachmargolis May 1, 2024
53e2efc
wip
zachmargolis May 1, 2024
fa5d240
i18n backend refactor miss
zachmargolis May 1, 2024
849aa37
patch script
zachmargolis May 1, 2024
22909b6
Add i18n-tasks backend
zachmargolis May 1, 2024
ccbf0bf
Merge remote-tracking branch 'origin/main' into margolis-i18n-txt-bac…
zachmargolis May 2, 2024
780b1dc
rails-18n-js plugin looks up nested keys (used for one, other)
zachmargolis May 3, 2024
2d97d76
Fix spec for trailing newline handling
zachmargolis May 3, 2024
28e25f7
Merge remote-tracking branch 'origin/main' into margolis-i18n-txt-bac…
zachmargolis May 3, 2024
ec94bb5
Optimize away an extra obj allocation
zachmargolis May 3, 2024
aeecaf8
Recover datetime translations
zachmargolis May 3, 2024
4c91c69
Recover more translation keys for en
zachmargolis May 3, 2024
6cc5dc5
Recover translations, update script
zachmargolis May 3, 2024
b007332
lint fixes
zachmargolis May 3, 2024
f6f5660
Add changelog
zachmargolis May 3, 2024
4cbd071
rubocop fixes
zachmargolis May 3, 2024
9669bc4
Remove some missing
zachmargolis May 3, 2024
8300bd8
remove extra translations
zachmargolis May 4, 2024
c82b9f8
lint fix
zachmargolis May 6, 2024
e77b049
make normalize_yaml
zachmargolis May 6, 2024
06236d4
Merge remote-tracking branch 'origin/main' into margolis-i18n-txt-bac…
zachmargolis May 6, 2024
5b228cb
Remove unused
zachmargolis May 6, 2024
997075b
Merge remote-tracking branch 'origin/main' into margolis-i18n-txt-bac…
zachmargolis May 6, 2024
1e44c59
Remove forced quoting in YAML normalization
zachmargolis May 6, 2024
a334049
Revert Makefile change
zachmargolis May 6, 2024
c7a1ab8
Update lib/i18n_flat_yml_backend.rb
zachmargolis May 7, 2024
3661955
Merge remote-tracking branch 'origin/main' into margolis-i18n-txt-bac…
zachmargolis May 8, 2024
c3a21d5
Use YAML array syntax, remove array conversion code
zachmargolis May 8, 2024
d48b20e
Update documentation
zachmargolis May 8, 2024
f30a45b
Rename confusing variable
zachmargolis May 8, 2024
7c251c5
Specs for custom i18n backend
zachmargolis May 8, 2024
2104ce6
make normalize_yaml
zachmargolis May 8, 2024
40f0540
i18n-tasks unused
zachmargolis May 8, 2024
000e9d7
add guard for nil config from running spec/lib/deploy/activate_spec.rb
zachmargolis May 8, 2024
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
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
"printWidth": 100,
"proseWrap": "never"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will affect all Markdown formatted by Prettier. I don't have a strong feeling one way or the other, just noting it has a more widespread impact than just i18n strings.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep these .yml as un-wrapped as possible, so I could back this change out but add it to normalize-yaml script as an override?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine if we acknowledge this as a project-wide default, which seems reasonable enough?

But yeah, if needed, I think this could also go back to being able to configure normalize-yaml with a specific Prettier configuration if we wanted to behave differently specific for normalize-yaml / specific for specific sets of files normalized by normalize-yaml.

}
8 changes: 8 additions & 0 deletions app/javascript/packages/normalize-yaml/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ describe('normalize', () => {
expect(await normalize(original, { prettierConfig })).to.equal(expected);
});

it('allows leaving prose un-wrapped', async () => {
const original =
'---\nfoo: "some very long key that would normally go past 100 characters and get line wrapped but is going to stay on the same line"';
const prettierConfig = { singleQuote: false, proseWrap: 'never' };

expect((await normalize(original, { prettierConfig })).trimEnd()).to.equal(original);
});

it('allows formatting with excluded formatters', async () => {
const original = '---\nmap:\n b: ...\n a: ...';
const expected = '---\nmap:\n a: ...\n b: ...\n';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { promises: fs } = require('fs');
const { promises: fs, readdirSync } = require('fs');
const { format } = require('util');
const path = require('path');
const YAML = require('yaml');
Expand All @@ -18,44 +18,6 @@ const ExtractKeysWebpackPlugin = require('./extract-keys-webpack-plugin.js');
* returning a string to use in its place.
*/

/**
* Returns the value in the object at the given key path.
*
* @example
* ```js
* const value = dig({ a: { b: { c: 'foo' } } }, ['a', 'b', 'c']);
* // 'foo'
* ```
*
* @param {undefined|Record<string, any>} object
* @param {string[]} keyPath
*
* @return {any}
*/
function dig(object, keyPath) {
let result = object;
for (const segment of keyPath) {
if (result == null) {
return;
}

result = result[segment];
}

return result;
}

/**
* Returns unique values from the given array.
*
* @template V
*
* @param {V[]} values
*
* @returns {V[]}
*/
const uniq = (values) => [...new Set(values)];

/**
* Returns truthy values from the given array.
*
Expand All @@ -67,34 +29,6 @@ const uniq = (values) => [...new Set(values)];
*/
const compact = (values) => /** @type {V[]} */ (values.filter(Boolean));

/**
* Returns the given key as a path of parts.
*
* @param {string} key
*
* @return {string[]}
*/
const getKeyPath = (key) => key.split('.');

/**
* Returns domain for a key string, or split key path.
*
* @param {string|string[]} keyOrKeyPath
*
* @return {string} The domain.
*/
const getKeyDomain = (keyOrKeyPath) =>
(Array.isArray(keyOrKeyPath) ? keyOrKeyPath : getKeyPath(keyOrKeyPath))[0];

/**
* Returns unique domains for the given set of keys.
*
* @param {string[]} keys
*
* @return {string[]}
*/
const getKeyDomains = (keys) => uniq(keys.map(getKeyDomain));

/**
* @extends {ExtractKeysWebpackPlugin<RailsI18nWebpackPluginOptions>}
*/
Expand All @@ -111,48 +45,44 @@ class RailsI18nWebpackPlugin extends ExtractKeysWebpackPlugin {
* Cached locale data.
*
* @type {{
* [domain: string]: {
* [locale: string]: Promise<{ [key: string]: string }>
* }
* [locale: string]: Promise<{ [key: string]: string }>
* }}
*/
localeData = Object.create(null);

/**
* Given a translation domain and locale, returns the file path corresponding to locale data.
*
* @param {string} domain
* @param {string} locale
*
* @return {string}
* @return {string[]}
*/
getLocaleFilePath(domain, locale) {
return path.resolve(this.options.configPath, domain, `${locale}.yml`);
getLocaleFilePaths(locale) {
return /** @type {string[]} */ (readdirSync(this.options.configPath, { recursive: true }))
.filter((/** @type {string} */ filePath) => filePath.endsWith(`${locale}.yml`))
.map((filePath) => path.resolve(this.options.configPath, filePath));
}

/**
* Returns a promise resolving to parsed YAML data for the given domain and locale.
*
* @param {string} domain
* @param {string} locale
*
* @return {Promise<undefined|Record<string, string>>}
*/
getLocaleData(domain, locale) {
if (!(domain in this.localeData)) {
this.localeData[domain] = Object.create(null);
}

if (!(locale in this.localeData[domain])) {
const localePath = this.getLocaleFilePath(domain, locale);

this.localeData[domain][locale] = fs
.readFile(localePath, 'utf-8')
.then(YAML.parse)
.catch(() => {});
getLocaleData(locale) {
if (!(locale in this.localeData)) {
this.localeData[locale] = Promise.all(
this.getLocaleFilePaths(locale).map((filePath) =>
fs
.readFile(filePath, 'utf-8')
.then(YAML.parse)
.catch(() => {}),
),
).then((fileDatas) => /** @type {Record<string, string>} */ Object.assign({}, ...fileDatas));
}

return this.localeData[domain][locale];
return this.localeData[locale];
}

/**
Expand All @@ -162,16 +92,28 @@ class RailsI18nWebpackPlugin extends ExtractKeysWebpackPlugin {
* @param {string} locale
* @param {MissingStringCallback} onMissingString
*
* @return {Promise<string>}
* @return {Promise<string|Record<string, string>>}
*/
async resolveTranslation(key, locale, onMissingString = this.options.onMissingString) {
const keyPath = getKeyPath(key);
const domain = getKeyDomain(keyPath);
const localeData = await this.getLocaleData(domain, locale);
const localeData = await this.getLocaleData(locale);

/** @type {undefined | string | Record<string,string>} */
let translation = localeData?.[key];

// Prefix search localeData, used in ".one", ".other" keys
if (translation === undefined && typeof localeData === 'object') {
const prefix = `${key}.`;
const prefixedEntries = Object.entries(localeData)
.filter(([localeDataKey]) => localeDataKey.startsWith(prefix))
.map(([localeDataKey, value]) => [localeDataKey.replace(prefix, ''), value]);

if (prefixedEntries.length) {
translation = Object.fromEntries(prefixedEntries);
}
}

let translation = dig(localeData, [locale, ...keyPath]);
if (translation === undefined) {
translation = onMissingString(key, locale);
translation = /** @type {string|undefined} */ (onMissingString(key, locale));
}

if (translation === undefined && locale !== this.options.defaultLocale) {
Expand All @@ -184,42 +126,28 @@ class RailsI18nWebpackPlugin extends ExtractKeysWebpackPlugin {
/**
* Returns a promise resolving to unique locales for the given domain.
*
* @param {string} domain
*
* @return {Promise<string[]>}
*/
async getDomainLocales(domain) {
const localeFiles = await fs.readdir(path.resolve(this.options.configPath, domain));
async getLocales() {
const localeFiles = await fs.readdir(this.options.configPath);

return localeFiles
.filter((file) => file.endsWith('.yml'))
.map((file) => path.basename(file, '.yml'));
}

/**
* Returns a promise resolving to the unique set of locales for the set of keys.
*
* @param {string[]} keys
*
* @return {Promise<string[]>}
*/
async getLocales(keys) {
const domains = getKeyDomains(keys);
return uniq((await Promise.all(domains.map((domain) => this.getDomainLocales(domain)))).flat());
}

/**
*
* @param {string[]} keys
* @param {string} locale
*
* @return {Promise<Record<string,string>|undefined>}
* @return {Promise<Record<string,string|Record<string,string>>|undefined>}
*/
async getTranslationData(keys, locale) {
/**
* @param {string} key
*
* @return {Promise<[key: string, string: string]>}
* @return {Promise<[key: string, string: string|Record<string, string>]>}
*/
const getKeyTranslationPairs = async (key) => [key, await this.resolveTranslation(key, locale)];

Expand All @@ -230,7 +158,7 @@ class RailsI18nWebpackPlugin extends ExtractKeysWebpackPlugin {
}

async getAdditionalAssets(keys) {
const locales = await this.getLocales(keys);
const locales = await this.getLocales();

/**
* @param {string} locale
Expand All @@ -249,9 +177,4 @@ class RailsI18nWebpackPlugin extends ExtractKeysWebpackPlugin {
}

module.exports = RailsI18nWebpackPlugin;
module.exports.dig = dig;
module.exports.uniq = uniq;
module.exports.compact = compact;
module.exports.getKeyPath = getKeyPath;
module.exports.getKeyDomain = getKeyDomain;
module.exports.getKeyDomains = getKeyDomains;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const webpack = require('webpack');
const WebpackAssetsManifest = require('webpack-assets-manifest');
const RailsI18nWebpackPlugin = require('./rails-i18n-webpack-plugin.js');

const { dig, uniq, compact, getKeyPath, getKeyDomain, getKeyDomains } = RailsI18nWebpackPlugin;
const { compact } = RailsI18nWebpackPlugin;

describe('RailsI18nWebpackPlugin', () => {
it('generates expected output', (done) => {
Expand Down Expand Up @@ -163,38 +163,6 @@ describe('RailsI18nWebpackPlugin', () => {
});
});

describe('dig', () => {
it('returns undefined when called on a nullish object', () => {
const object = undefined;
const result = dig(object, ['a', 'b']);

expect(result).to.be.undefined();
});

it('returns undefined when path is unreachable', () => {
const object = {};
const result = dig(object, ['a', 'b']);

expect(result).to.be.undefined();
});

it('returns value at path', () => {
const object = { a: { b: 1 } };
const result = dig(object, ['a', 'b']);

expect(result).to.be.equal(1);
});
});

describe('uniq', () => {
it('returns unique values', () => {
const values = [1, 2, 2, 3];
const result = uniq(values);

expect(result).to.deep.equal([1, 2, 3]);
});
});

describe('compact', () => {
it('returns truthy values', () => {
const values = [1, 0, null, false];
Expand All @@ -203,43 +171,3 @@ describe('compact', () => {
expect(result).to.deep.equal([1]);
});
});

describe('getKeyPath', () => {
it('returns key path parts', () => {
const key = 'a.b.c';
const result = getKeyPath(key);

expect(result).to.deep.equal(['a', 'b', 'c']);
});
});

describe('getKeyDomain', () => {
context('key', () => {
const key = 'a.b.c';

it('returns domain', () => {
const result = getKeyDomain(key);

expect(result).to.equal('a');
});
});

context('key path', () => {
const keyPath = ['a', 'b', 'c'];

it('returns domain', () => {
const result = getKeyDomain(keyPath);

expect(result).to.equal('a');
});
});
});

describe('getKeyDomains', () => {
it('returns unique set of domains for keys', () => {
const keys = ['a.b.c', 'a.d.e', 'b.f.g'];
const domains = getKeyDomains(keys);

expect(domains).to.deep.equal(['a', 'b']);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
forms.button.cancel: "Cancel"
forms.button.reset: "Reset"
forms.button.submit: "Submit"
forms.dynamic: "Dynamic"
forms.key1: "value1-en"
forms.key2: "value2-en"
forms.messages.one: "One message"
forms.messages.other: "%{count} messages"
item.1: "First"
item.2: "Second"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
forms.button.cancel: "Cancelar"
forms.button.reset: "Reiniciar"
forms.button.submit: "Enviar"
forms.dynamic: "Dinámico"
forms.key1: "value1-es"
forms.key2: "value2-es"
forms.messages.one: "Un mensaje"
forms.messages.other: "%{count} mensajes"
Loading