Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.

Commit cb50cc9

Browse files
committed
fix: dynamic translation key with prefixes and add tests
1 parent 477e9ae commit cb50cc9

6 files changed

Lines changed: 127 additions & 32 deletions

File tree

src/utilities/locales.ts

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ function extractSchemaTranslationKeys(content: string): Set<string> {
112112
return keys
113113
}
114114

115+
function extractDynamicTranslationPrefixes(keys: Set<string>): Set<string> {
116+
const prefixKeys = new Set<string>()
117+
for (const key of keys) {
118+
if (key.endsWith('.')) {
119+
prefixKeys.add(key)
120+
}
121+
}
122+
123+
return prefixKeys
124+
}
125+
115126
function extractStorefrontTranslationKeys(content: string): Set<string> {
116127
const keys = new Set<string>()
117128

@@ -133,22 +144,21 @@ function extractStorefrontTranslationKeys(content: string): Set<string> {
133144
}
134145
}
135146

136-
// Find dynamic translation keys (string literal followed by filters ending with t filter)
137-
// This captures patterns like: 'prefix.' | append: x | replace: '-', '_' | t
138-
const dynamicPatterns = [
139-
// {% assign/capture var = 'prefix' | filters | t %}
140-
/{%\s*(?:assign|capture)\s+\w+\s*=\s*["']([^"']+)["']\s*\|.*?\|\s*t[^%]*%}/g,
141-
// {{ 'prefix' | filters | t }}
142-
/{{\s*-?\s*["']([^"']+)["']\s*\|.*?\|\s*t[^}]*-?\s*}}/g,
143-
]
147+
// Find dynamic translation keys (any string ending with a dot followed by t filter)
148+
// This captures patterns like:
149+
// - 'prefix.' | append: x | t
150+
// - 'prefix.' | append: x | replace: '-', '_' | t
151+
// - assign tag_text = 'tags.' | append: tag_id | t
152+
// - {% assign tag_text = 'tags.' | append: tag_id | t %}
153+
// - {{ 'prefix.' | append: x | t }}
154+
const dynamicPattern = /["']([^"']*\.)["'](?:[^|]*\|)+(?:[^t|]*\|)?\s*t(?::[^%}]*)?[%}]?/g;
144155

145-
for (const pattern of dynamicPatterns) {
146-
const matches = content.match(pattern) || []
147-
for (const match of matches) {
148-
const keyMatch = match.match(/["']([^"']+)["']/)
149-
if (keyMatch && keyMatch[1]) {
150-
keys.add(keyMatch[1])
151-
}
156+
const matches = content.match(dynamicPattern) || [];
157+
for (const match of matches) {
158+
const keyMatch = match.match(/["']([^"']*\.)["']/);
159+
if (keyMatch && keyMatch[1]) {
160+
const key = keyMatch[1];
161+
keys.add(key);
152162
}
153163
}
154164

@@ -275,10 +285,28 @@ function removeUnreferencedKeysFromFile(filePath: string, usedKeys: Set<string>,
275285
const flattenedContent = flattenObject(content)
276286
const cleanedContent: Record<string, unknown> = {}
277287

288+
// Extract prefix keys
289+
const prefixKeys = extractDynamicTranslationPrefixes(usedKeys)
290+
291+
// Process exact key matches and base paths
278292
for (const [key, value] of Object.entries(flattenedContent)) {
279293
const basePath = key.split('.').slice(0, -1).join('.')
280294
if (usedKeys.has(key) || usedKeys.has(basePath)) {
281295
cleanedContent[key] = value
296+
continue
297+
}
298+
299+
// Check if this key matches any of the prefix patterns
300+
let matchesPrefix = false
301+
for (const prefix of prefixKeys) {
302+
if (key.startsWith(prefix)) {
303+
matchesPrefix = true
304+
break
305+
}
306+
}
307+
308+
if (matchesPrefix) {
309+
cleanedContent[key] = value
282310
}
283311
}
284312

@@ -419,6 +447,9 @@ function filterContentByKeys(
419447
): Record<string, unknown> {
420448
const filteredContent: Record<string, unknown> = {}
421449

450+
// Extract prefix keys
451+
const prefixKeys = extractDynamicTranslationPrefixes(requiredKeys)
452+
422453
// Process exact key matches
423454
for (const key of requiredKeys) {
424455
if (key in flatContent) {
@@ -427,23 +458,13 @@ function filterContentByKeys(
427458
}
428459

429460
// Handle prefix matches for dynamic keys
430-
for (const key of requiredKeys) {
431-
if (key.endsWith('.')) {
432-
addPrefixMatches(flatContent, filteredContent, key)
461+
for (const prefix of prefixKeys) {
462+
for (const sourceKey of Object.keys(flatContent)) {
463+
if (sourceKey.startsWith(prefix) && !(sourceKey in filteredContent)) {
464+
filteredContent[sourceKey] = flatContent[sourceKey]
465+
}
433466
}
434467
}
435468

436469
return filteredContent
437470
}
438-
439-
function addPrefixMatches(
440-
flatContent: Record<string, unknown>,
441-
filteredContent: Record<string, unknown>,
442-
prefix: string
443-
): void {
444-
for (const sourceKey of Object.keys(flatContent)) {
445-
if (sourceKey.startsWith(prefix) && !(sourceKey in filteredContent)) {
446-
filteredContent[sourceKey] = flatContent[sourceKey]
447-
}
448-
}
449-
}

test/commands/theme/locale/clean.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,18 @@ describe('theme locale clean', () => {
142142
}
143143
}
144144
})
145+
146+
it('preserves dynamic translation keys with prefixes', async () => {
147+
await runCommand(['theme', 'locale', 'sync', '--locales-dir', path.join(fixturesPath, 'locales'), '--clean'])
148+
149+
const storefrontContent = JSON.parse(fs.readFileSync(path.join(testThemeLocalesPath, 'en.default.json'), 'utf8'))
150+
151+
expect(storefrontContent).to.have.nested.property('tags.new')
152+
expect(storefrontContent).to.have.nested.property('tags.sale')
153+
expect(storefrontContent).to.have.nested.property('tags.featured')
154+
expect(storefrontContent).to.have.nested.property('tags.custom')
155+
expect(storefrontContent).to.have.nested.property('tags.special')
156+
157+
expect(storefrontContent).to.not.have.property('unused')
158+
})
145159
})

test/commands/theme/locale/sync.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,43 @@ describe('theme locale sync', () => {
166166
const storefrontContent = JSON.parse(fs.readFileSync(storefrontFilePath, 'utf8'))
167167
const schemaContent = JSON.parse(fs.readFileSync(schemaFilePath, 'utf8'))
168168

169-
// Verify that unreferenced translations from source are not added
170169
expect(storefrontContent).to.not.have.nested.property('additional.new_key')
171170
expect(schemaContent).to.not.have.nested.property('additional.new_setting')
172171

173-
// Verify that referenced translations are still present
174172
expect(storefrontContent).to.have.nested.property('actions.add_to_cart')
175173
expect(storefrontContent).to.have.nested.property('t_with_fallback.direct_key')
176174
expect(storefrontContent).to.have.nested.property('t_with_fallback.variable_key')
177175
expect(schemaContent).to.have.nested.property('section.name')
178176
expect(schemaContent).to.have.nested.property('section.settings.logo_label')
179177
})
178+
179+
it('syncs dynamic translation keys with prefixes', async () => {
180+
await runCommand(['theme', 'locale', 'sync', '--locales-dir', localesPath])
181+
182+
const storefrontContent = JSON.parse(fs.readFileSync(path.join(testThemeLocalesPath, 'en.default.json'), 'utf8'))
183+
184+
expect(storefrontContent).to.have.nested.property('tags.new')
185+
expect(storefrontContent).to.have.nested.property('tags.sale')
186+
expect(storefrontContent).to.have.nested.property('tags.featured')
187+
expect(storefrontContent).to.have.nested.property('tags.custom')
188+
expect(storefrontContent).to.have.nested.property('tags.special')
189+
190+
expect(storefrontContent.tags.new).to.equal(sourceEnDefault.tags.new)
191+
192+
const frStorefrontContent = JSON.parse(fs.readFileSync(path.join(testThemeLocalesPath, 'fr.json'), 'utf8'))
193+
expect(frStorefrontContent).to.have.nested.property('tags.new')
194+
expect(frStorefrontContent.tags.new).to.equal(sourceFr.tags.new)
195+
})
196+
197+
it('cleans dynamic translation keys but keeps referenced prefixes', async () => {
198+
await runCommand(['theme', 'locale', 'sync', '--locales-dir', localesPath, '--clean'])
199+
200+
const storefrontContent = JSON.parse(fs.readFileSync(path.join(testThemeLocalesPath, 'en.default.json'), 'utf8'))
201+
202+
expect(storefrontContent).to.have.nested.property('tags.new')
203+
204+
// These should also be kept because they share the same prefix
205+
expect(storefrontContent).to.have.nested.property('tags.sale')
206+
expect(storefrontContent).to.have.nested.property('tags.featured')
207+
})
180208
})

test/fixtures/locales/en.default.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,12 @@
88
"t_with_fallback": {
99
"direct_key": "Direct key (source)",
1010
"variable_key": "Variable key (source)"
11+
},
12+
"tags": {
13+
"new": "New",
14+
"sale": "Sale",
15+
"featured": "Featured",
16+
"custom": "Custom Tag",
17+
"special": "Special Tag"
1118
}
1219
}

test/fixtures/locales/fr.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,12 @@
88
"t_with_fallback": {
99
"direct_key": "Clé directe (source)",
1010
"variable_key": "Clé variable (source)"
11+
},
12+
"tags": {
13+
"new": "Nouveau",
14+
"sale": "En solde",
15+
"featured": "En vedette",
16+
"custom": "Personnalisée",
17+
"special": "Spéciale"
1118
}
1219
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{% # Basic dynamic translation with append %}
2+
{{ 'tags.' | append: 'new' | t }}
3+
4+
{% # Dynamic translation in HTML element %}
5+
<span>{{ 'tags.' | append: 'sale' | t }}</span>
6+
7+
{% # Dynamic translation with multiple filters %}
8+
{{ 'tags.' | append: 'featured' | replace: '-', '_' | t }}
9+
10+
{% # Dynamic translation with liquid tag assign %}
11+
{% assign tag_id = 'custom' %}
12+
{% assign tag_text = 'tags.' | append: tag_id | t %}
13+
14+
{% # Dynamic translation with liquid tag block %}
15+
{% liquid
16+
assign special_tag = 'special'
17+
assign special_tag_text = 'tags.' | append: special_tag | t
18+
%}

0 commit comments

Comments
 (0)